+The features of FastTrack can be divided into 4 groups, **Category Features**, **Expense Features**, **General Features** and **Expense Statistics Feature**. With these 4 groups in mind, remembering the different commands becomes extremely convenient, as each group contains mainly 4 types of operations - add, delete, edit and list!
+
+### Commands
+
+| [**Category**](#category-features) | [**One-Time Expenses**](#one-time-expenses) | [**Recurring Expenses**](#recurring-expenses) | [**General**](#general-features) |
+| --------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------- |
+| [Add a category](#adding-a-category-addcat) | [Add an expense](#adding-an-expense-add) | [Add a recurring expense](#adding-a-recurring-expense-addrec) | [Set a budget](#setting-a-budget-set) |
+| [Edit a category](#editing-a-category-edcat) | [Edit an expense](#editing-an-expense-edexp) | [Edit a recurring expense](#editing-a-recurring-expense-edrec) | [Category autocompletion](#category-autocompletion) |
+| [Delete a category](#deleting-a-category-delcat) | [Delete an expense](#deleting-an-expense-delete) | [Delete a recurring expense](#deleting-a-recurring-expense-delrec) | [Clear all entries](#clearing-all-entries-clear) |
+| [List categories](#listing-categories-lcat) | [Find an expense by keyword](#search-for-an-expense-by-keyword-find) | [List recurring expenses](#listing-recurring-expenses-lrec) | [Exit FastTrack](#exiting-fasttrack-exit) |
+| [View category summary](#viewing-category-summary-sumcat) | [List expenses](#listing-expenses-list) | | [View help](#viewing-help-help) |
+
+
+### Other Notable Features
+
+| [**Expense Statistics Feature**](#expense-statistics-feature) |
+| ----------------------------------------------------------------------------------- |
+| [Monthly spending statistic](#monthly-spending-statistic) |
+| [Monthly remaining statistic](#monthly-remaining-statistic) |
+| [Monthly percentage change statistic](#monthly-percentage-change-statistic) |
+| [Weekly spending statistic](#weekly-spending-statistic) |
+| [Weekly remaining statistic](#weekly-remaining-statistic) |
+| [Weekly percentage change statistic](#weekly-percentage-change-statistic) |
+| [Total spent statistic](#total-spent-statistic) |
+| [Budget utilisation percentage statistic](#budget-utilisation-percentage-statistic) |
+
+## Command Syntax
+
+First-time users may have difficulty understanding the syntax described in the command instructions.
+
+If you are new to using a **Command Line Interface (CLI)**, we recommend reading this brief section before using FastTrack.
+Understanding the **CLI** will help you enter commands more efficiently, which can save you time in the long run.
+
+In simple terms, the **Command Line Interface (CLI)** is a way to interact with FastTrack by typing in commands using just one line of text. This means you can add expenses quickly and easily.
-**:information_source: Notes about the command format:**
+Here is a quick guide on how to read the syntax mentioned in the User Guide for using FastTrack's commands.
-* 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`.
+```
+command tag1/ PARAMETER_1 tag2/ PARAMETER_2 [tag3/ PARAMETER_3]
+```
-* 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`.
+| Element | Format | Usage |
+| ----------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `command` | Name of command
eg. `add`, `find` | Specifies the command to be executed. |
+| `tag/` | Prefix for a field, followed by `/` | Specifies which field given input argument is for |
+| `PARAMETER` | Words that are in UPPERCASE | Specifies user input for field specified by `tag/`
e.g.
In `add c/CATEGORY_NAME`, `CATEGORY_NAME` is a parameter, which the user decides is “groceries”. The final command will be entered as add `c/groceries`. |
+| `[]` | Square brackets around `tag/` and `PARAMETER` | Indicates that field specified by `tag/` is optional and may be omitted.
If left unspecified, it will be set to a default value. |
-* Items with `…` after them can be used multiple times including zero times.
- e.g. `[t/TAG]…` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc.
+For example, the command format for `add`:
-* 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.
+```
+add c/CATEGORY_NAME n/ITEM_NAME p/PRICE [d/DATE]
+```
-* If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken.
+- `add` is the `command` name.
+- `c/`, `n/`, `p/`, `d/` are `tag/`s to denote fields of _category_, _name_, _price_ and _date_ respectively.
+- `CATEGORY_NAME`, `ITEM_NAME`, `PRICE`, `DATE` are `PARAMETERS` to be supplied to the aforementioned `tag/`s.
+- `[d/DATE]` indicates that the field for the date is optional.
-* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+Don't worry if it takes a bit of time to get used to the commands. Once you're familiar with the commands, you'll be able to add expenses quickly and easily.
+
+
-Shows a message explaning how to access the help page.
+**:warning: Warning about Date Formatting:**
+Some commands may include the entry of a `DATE`, like the `add` command or the `addrec` command. A specific date format, `Day/Month/Year` should be used to format the date provided.
-![help message](images/helpMessage.png)
+However, if the day is **within 1-31** but **not a valid date**, _e.g. 30th February_, FastTrack will smartly correct your date provided to the last date of the month.
i.e, `30/2/2023` will be corrected to `28/2/2023`.
-Format: `help`
+
+---
+
+# **Category Features**
+
+FastTrack makes it easy for you to keep track of your spending by organizing expenses into categories. A category is like a folder that holds all your expenses that fall under a specific theme. For example, you might have a category called Groceries where you record all purchases from Fairprice or NTUC.
-### Adding a person: `add`
+To create a category in FastTrack, simply give it a name, such as Entertainment or Transportation. You can also add a short text summary to give yourself more context about the category.
-Adds a person to the address book.
+FastTrack even has a default Misc category for any expenses that you haven't categorized yet, however, this category is not modifiable or accessible.
-Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…`
+
+
+**:information_source: Info**
+Note that category names in FastTrack are case-insensitive. For example, a category named `Groceries` will be treated as the exact same category as `groceries`.
-
:bulb: **Tip:**
-A person can have any number of tags (including 0)
-Examples:
-* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`
-* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal`
+### Command Summary
+
+| Feature | Command Format | Examples |
+| -------------------------------------------------------- | ------------------------------------------- | ---------------------------------- |
+| [**List Categories**](#listing-categories-lcat) | `lcat` | `lcat` |
+| [**Add Category**](#adding-a-category-addcat) | `addcat c/CATEGORY_NAME s/SUMMARY` | `addcat c/Groceries s/for living` |
+| [**Delete Category**](#deleting-a-category-delcat) | `delcat INDEX` | `delcat 1` |
+| [**Edit Category**](#editing-a-category-edcat) | `edcat INDEX [c/CATEGORY_NAME] [s/SUMMARY]` | `edcat 1 c/New Name s/New Summary` |
+| [**Category Summary**](#viewing-category-summary-sumcat) | `sumcat INDEX` | `sumcat 2` |
-### Listing all persons : `list`
+---
-Shows a list of all persons in the address book.
+## **Listing Categories** `lcat`
-Format: `list`
+Displays the list of categories in FastTrack.
-### Editing a person : `edit`
+Format: `lcat`
-Edits an existing person in the address book.
+### Demonstration
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
+1. Type `lcat` into the command box
+2. FastTrack displays the list of categories with the confirmation message `Listed all categories`
-* 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.
+![FastTrack lcat](images/demo/category/lcat.png)
-Examples:
-* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively.
-* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags.
+## **Adding a category** `addcat`
-### Locating persons by name: `find`
+Adds a new category to FastTrack. If a category with the same name already exists, this command will not execute.
-Finds persons whose names contain any of the given keywords.
+Format: `addcat c/CATEGORY_NAME s/SUMMARY`
-Format: `find KEYWORD [MORE_KEYWORDS]`
+| Parameter | Description |
+| --------------- | --------------------------------------------------- |
+| `CATEGORY_NAME` | Title of the category to be added. |
+| `SUMMARY` | Short summary of what this category keeps track of. |
+
+### Examples
+
+- `addcat c/Groceries s/for living` creates a new `Groceries` category with the summary of `for living`.
+- `addcat c/Entertainment s/for fun!` creates a new `Entertainment` category with the summary of `for fun!`.
+
+### Demonstration
+
+1. Enter the command `lcat` to switch to the **Category Display**
+2. Enter the command `addcat c/Groceries s/for living` into the command box
+3. FastTrack adds the new category to the category list with the confirmation message `New category added: groceries`
+
+![FastTrack addcat](images/demo/category/addcat.png)
+
+## **Deleting a category** `delcat`
+
+Deletes the category at the specified `INDEX` in the category list.
+
+Format: `delcat INDEX`
+
+| Parameter | Description |
+| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `INDEX` | The index number shown in the displayed category list.
It must be a positive integer i.e. 1, 2, 3, ...
Expenses previously categorised under the specified category will be automatically re-categorized under the `Misc` category.
|
+
+
+**:information_source: Info:**
+
+If you delete a category that has existing expenses associated with it, those expenses will be automatically reassigned to the default internal `Misc` category. But don't worry! You can still re-assign them later with the [edit expense command](#editing-an-expense-edexp). To avoid losing track of expenses, we recommend that you review and update your categories periodically, rather than deleting them altogether.
+
+
+
+### Examples
-* The search is case-insensitive. e.g `hans` will match `Hans`
-* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans`
-* Only the name is searched.
-* Only full words will be matched e.g. `Han` will not match `Hans`
-* Persons matching at least one keyword will be returned (i.e. `OR` search).
- e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang`
+- `lcat` followed by `delcat 2` deletes the second category in the category list
+- `lcat` followed by `delcat 1` deletes the first category in the category list
-Examples:
-* `find John` returns `john` and `John Doe`
-* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png)
+### Demonstration
-### Deleting a person : `delete`
+1. Enter the command `lcat` to switch to the **Category Display**
+2. Enter the command `delcat 7` into the command box
+3. FastTrack deletes the seventh category `Food` from the category list with the confirmation message `Deleted category: food`
-Deletes the specified person from the address book.
+![FastTrack delcat](images/demo/category/delcat.png)
+
+## **Editing a category** `edcat`
+
+Edits the category at the specified `INDEX` in the category list.
+
+Format: `edcat INDEX [c/CATEGORY_NAME] [s/SUMMARY]`
+
+Both `CATEGORY_NAME` and `SUMMARY` are optional by themselves, but **at least** one of them must be specified in addition
+to `INDEX`, otherwise the command will not be executed.
+
+| Parameter | Description |
+| --------------- | --------------------------------------------------------------------------------------------------------- |
+| `INDEX` | The index of the category to be edited.
It must be a positive integer i.e. 1, 2, 3, ... |
+| `CATEGORY_NAME` | The new name of the category being edited at the specified index.
This parameter is optional. |
+| `SUMMARY` | The new summary of the category being edited at the specified index.
This parameter is optional. |
+
+### Examples
+
+- `edcat 1 c/Drink` changes the name of the first category in the category list to `Drink`
+- `edcat 2 c/Food s/Eating out` changes the name and summary of the second category in the category list to `Food` and `Eating out` respectively.
+
+### Demonstration
+
+1. Enter the command `lcat` to switch to the **Category Display**
+2. Enter the command `edcat 1 c/Drink` into the command box
+3. FastTrack edits the name of the first category `Food` from the category list to `Drink` with the confirmation message `Edited category: Drink`
+
+![FastTrack edcat1](images/demo/category/edcat1.png)
+![FastTrack edcat2](images/demo/category/edcat2.png)
+
+## **Viewing Category Summary** `sumcat`
+
+Displays the category summary for a category.
+
+Format: `sumcat INDEX`
+
+| Parameter | Description |
+| --------- | ------------------------------------------------------------------------------------------------ |
+| `INDEX` | The index of the category to be edited.
It must be a positive integer i.e. 1, 2, 3, ... |
+
+### Demonstration
+
+1. Enter the command `lcat` to switch to the **Category Display**
+2. Enter the command `sumcat 2` into the command box.
+3. FastTrack displays the summary of the Entertainment category in the Results Display.
+
+![FastTrack sumcat](images/demo/category/sumcat.png)
+
+
+ Back to Top
+
+
+---
+
+# **Expense Features**
+
+An **expense** is a single purchase that you want to track. Each expense has a _name_, _price_, _category_, and _date_. With FastTrack, you can easily duplicate an expense if you happen to make the same purchase multiple times, such as buying a coffee from CoolSpot every morning on your way to NUS.
+
+Finally, there are **recurring expenses**. These are expenses that are charged on a regular basis, such as a monthly subscription to Netflix or an annual Heroku subscription. Instead of manually creating an expense every time the payment is due, you can set up a recurring expense in FastTrack.
+Simply specify the start date, interval (daily, weekly, monthly, yearly), and end date (if applicable), and FastTrack will automatically generate the expenses for you.
+
+### Command Summary
+
+#### One-time Expenses
+
+| Feature | Command Format | Examples |
+| ---------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------- |
+| [**List Expenses**](#listing-expenses-list) | `list [c/CATEGORY_NAME] [t/TIMEFRAME]` | `list c/Food t/month` |
+| [**Add Expense**](#adding-an-expense-add) | `add c/CATEGORY_NAME n/ITEM_NAME p/PRICE [d/DATE]` | `add c/Food p/20 n/Mac d/14/2/23` |
+| [**Delete Expense**](#deleting-an-expense-delete) | `delete INDEX` | `delete 1` |
+| [**Edit Expense**](#editing-an-expense-edexp) | `edexp INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [d/DATE] [p/PRICE]` | `edexp 1 c/Food n/Mac d/20/4/23 p/10` |
+| [**Find Expense**](#search-for-an-expense-by-keyword-find) | `find KEYWORD [MORE_KEYWORDS]` | `find KFC chicken` |
+
+#### Recurring Expenses
+
+| Feature | Command Format | Example |
+| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
+| [**List Recurring Expense**](#listing-recurring-expenses-lrec) | `lrec` | `lrec` |
+| [**Add Recurring Expense**](#adding-a-recurring-expense-addrec) | `addrec c/CATEGORY_NAME n/ITEM_NAME p/PRICE t/INTERVAL sd/START_DATE [ed/END_DATE]` | `addrec c/Shows n/Netflix p/10 t/month sd/10/3/23 ed/10/03/24` |
+| [**Delete Recurring Expense**](#deleting-a-recurring-expense-delrec) | `delrec INDEX` | `delrec 1` |
+| [**Edit Recurring Expense**](#editing-a-recurring-expense-edrec) | `edrec INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [p/PRICE] [t/INTERVAL] [ed/END_DATE]` | `edrec 1 c/Show n/Disney Plus p/2 t/week ed/10/5/24` |
+
+---
+
+## **One-Time Expenses**
+
+---
+
+## **Listing expenses** `list`
+
+The `list` feature in FastTrack allows you to view all your expenses. You can filter the list based on specific categories and timeframes to get a more customized view of your spending.
+
+If you apply the `CATEGORY_NAME` filter, only expenses associated with that particular category will be displayed. For instance, if you filter by `groceries`, you'll only see the expenses you've categorized as `groceries`.
+
+If you apply the `TIMEFRAME` filter, you can see expenses that fall within a particular time period. For example, you could filter by `month` to see only the expenses you incurred in the current month.
+
+If you don't specify any filters, the expense list will show all your expenses by default.
+
+Format: `list [c/CATEGORY_NAME] [t/TIMEFRAME] [r/RECUR_PERIOD]`
+
+| Parameter | Description |
+| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `CATEGORY_NAME` | The name of the category of which expenses are classed under.
This parameter is optional. |
+| `TIMEFRAME` | The timeframe of which expenses were added.
The timeframes available are:
1. `week` (alias: `w`)
2. `month` (alias: `m`)
3. `year` (alias: `y`)
This parameter is optional. |
+
+
+**:information_source: What is a timeframe?**
+
+A `TIMEFRAME` allows you to set a specific interval to filter your expenses. `t/w` or `t/week` is a timeframe representing the current week, while `t/m` or `t/month` is a timeframe representing the current month, and `t/y` or `t/year` is a timeframe representing the current year.
+
+
+
+### Examples
+
+- `list`
+- `list c/Groceries t/week`
+- `list c/Entertainment t/month`
+- `list c/Food`
+- `list t/w`
+- `list c/Entertainment t/year`
+
+### Demonstration
+
+### **List Expenses by Category**
+
+1. Enter the command `list c/drink` into the command box
+2. FastTrack displays the expenses under the category `Drink` with the confirmation message `2 expenses listed`. The number of expenses may differ for every user.
+
+![FastTrack list1](images/demo/expense/list1.png)
+
+### **List All Expenses**
+
+1. Enter the command `list` into the command box
+2. FastTrack displays all expenses with the confirmation message `5 expenses listed`. The number of expenses may differ for every user.
+
+![FastTrack list2](images/demo/expense/list2.png)
+
+### **List Expenses by Timeframe**
+
+1. Enter the command `list t/w` into the command box
+2. FastTrack displays all expenses within the current week with the confirmation message `2 expenses listed`. The number of expenses may differ for every user.
+
+![FastTrack list3](images/demo/expense/list3.png)
+
+
+
+**:information_source: Using both `CATEGORY_NAME` and `TIMEFRAME` filters:**
+
+- Using both the category and timeframe filters will only display expenses that satisfy both the filter conditions.
+e.g. in `list c/food t/week`, only expenses with both the category name "Food" and date falling within the current week will be displayed.
+
+
+## **Adding an expense** `add`
+
+Adds a new one-time expense to FastTrack.
+
+Format: `add c/CATEGORY_NAME n/ITEM_NAME p/PRICE [d/DATE]`
+
+| Parameter | Description |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `CATEGORY_NAME` | The category which the expense should be classified under.
If there is no such category in FastTrack, a new category will be created with the specified category name. |
+| `ITEM_NAME` | Name of the expense being added. |
+| `PRICE` | The price of the expense being added.
The specified price should be a number, e.g. 4, 4.50. |
+| `DATE` | The date of the expense being added.
The date format should be d/m/yyyy.
This is an optional input, and if left unspecified, the date of the expense will be set to the **current date** by default. |
+
+### Examples
+
+- `add c/groceries n/milk p/4.50 `
+- `add c/entertainment p/20 n/movie night d/14/2/23`
+
+### Demonstration
+
+1. Enter the command `add c/groceries n/milk p/4.50` into the command box
+2. FastTrack adds the new expense under the new category `Groceries` with the confirmation message `New expense added: Name: milk, Amount: $4.5, Date: 2023-04-07, Category: groceries`.
+
+![FastTrack add1](images/demo/expense/add1.png)
+
+3. Enter the command `lcat` to switch to the **Category Display**. Notice how FastTrack has automatically created a new category `Groceries` in the category list!
+
+![FastTrack add2](images/demo/expense/add2.png)
+
+## **Deleting an expense** `delete`
+
+Deletes an expense at the specified `INDEX` in the expense list.
Format: `delete INDEX`
-* Deletes the person at the specified `INDEX`.
-* The index refers to the index number shown in the displayed person list.
-* The index **must be a positive integer** 1, 2, 3, …
+| Parameter | Description |
+| --------- | ----------------------------------------------------------------------------------------------------------------- |
+| `INDEX` | The index number shown in the displayed categories list.
It must be a positive integer i.e. 1, 2, 3, ... |
+
+### Examples
+
+- `list` followed by `delete 2` deletes the second expense in the expense list
+- `find movie` followed by `delete 1` deletes the first expense in the results of the `find` command
+
+### Demonstration
+
+1. Type the command `list` to switch to the **Expense Display**
+2. Enter the command `delete 2` into the command box
+3. FastTrack deletes the second expense in the expense list with the confirmation message `Deleted expense: Name: milk, Amount: $4.5, Date: 2023-04-07, Category: groceries`.
+
+
+
+![FastTrack delete](images/demo/expense/delete.png)
+
+
+## **Editing an expense** `edexp`
+
+Edits the expense at the specified `INDEX` in the expense list.
+
+Format: `edexp INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [d/DATE] [p/PRICE] [r/RECUR_PERIOD]`
+
+Every parameter except for `INDEX` is optional by themselves, but **at least** one of them must be specified in addition
+to `INDEX`, otherwise the command will not be executed.
+
+| Parameter | Description |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `INDEX` | The index of the expense to be edited.
It must be a positive integer i.e. 1, 2, 3, ... |
+| `CATEGORY_NAME` | The new category name of the expense to be changed to.
This parameter is optional. |
+| `EXPENSE_NAME` | The new expense name of the expense to be changed to.
This parameter is optional. |
+| `DATE` | The new date of the expense to be changed to.
The date format should be d/m/yyyy.
This parameter is optional. |
+| `PRICE` | The new price of the expense to be changed to.
The specified price should be a number, e.g. 4, 4.50.
This parameter is optional. |
+
+### Examples
+
+- `edexp 1 c/groceries` changes the category of the first expense in the expense tracker
+- `edexp 2 p/20 n/movie night` changes the price and name of the second expense in the expense tracker
+
+### Demonstration
+
+1. Type the command `list` to switch to the **Expense Display**
+2. Enter the command `edexp 2 p/20 n/movie night c/entertainment`
+3. FastTrack edits the second expense in the expense list with the confirmation message `Edited expense: Name: movie night, Amount: $20.0, Date: 2023-04-03, Category: entertaiment`.
-Examples:
-* `list` followed by `delete 2` deletes the 2nd person in the address book.
-* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command.
+![FastTrack edexp1](images/demo/expense/edexp1.png)
+![FastTrack edexp2](images/demo/expense/edexp2.png)
-### Clearing all entries : `clear`
+## **Search for an expense by keyword** `find`
-Clears all entries from the address book.
+Find expenses whose names contain any of the given keywords.
-Format: `clear`
+Format: `find KEYWORD [MORE_KEYWORDS]`
+
+- The search is case-insensitive. e.g. `dinner` will match `Dinner`
+- The order of the keywords does not matter. e.g. `ramen Dinner` will match `Dinner ramen`
+- Only the name of the expense is searched
+- Only full words will be matched e.g. `dinn` will not match `dinner`
+- Expenses matching at least one keyword will be returned
+ e.g. `movie dinner` will return `dinner with Alex`, `movie with friends`
+
+### Examples
+
+Suppose you have 3 expenses logged:
+
+```
+Date: 2023-03-02, Category: Food, Name: McDonald's, Price: $7.50
+Date: 2023-03-02, Category: Food, Name: KFC, Price: $6.00
+Date: 2023-03-03, Category: Groceries, Name: Milk, Price: $4.00
+```
+
+- `find kfc milk` returns `Milk` and `KFC`
+- `find mcdonald's` returns `McDonald's`
+
+### Demonstration
+
+1. Enter the command `find movie` into the command box to find expenses with the keyword `movie`
+2. FastTrack filters the expense list to show only the expenses matching the given keyword, with the confirmation message `Edited expense: Name: movie night, Amount: $20.0, Date: 2023-04-03, Category: entertaiment`.
+
+![FastTrack find](images/demo/expense/find.png)
+
+
+ Back to Top
+
+
+---
+
+## **Recurring Expenses**
+
+---
+
+## **Listing Recurring Expenses** `lrec`
+
+Displays the list of recurring expenses in FastTrack.
+
+Format: `lrec`
+
+### Demonstration
+
+1. Type `lrec` into the command box
+2. FastTrack displays the list of recurring expenses with the confirmation message `Listed all recurring expenses`
+
+![FastTrack lrec](images/demo/recurring_expense/lrec.png)
+
+## **Adding a Recurring Expense** `addrec`
+
+Adds a recurring expense to FastTrack.
+
+Format: `addrec c/CATEGORY_NAME n/ITEM_NAME p/PRICE t/INTERVAL sd/START_DATE [ed/END_DATE]`
+
+| Parameter | Description |
+| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `CATEGORY_NAME` | The category which the recurring expense should be classified under.
If there is no such category, a new category will be created with the specified category name. |
+| `ITEM_NAME` | Name of the recurring expense being added. |
+| `PRICE` | The price of the recurring expense being added.
The specified price should be a number, e.g. 4, 4.50. |
+| `INTERVAL` | The period with which the expense is recurring.
The timeframes available are:
1. `day`
2. `week`
3. `month`
4. `year` |
+| `START_DATE` | The starting date of the recurring expense.
The date format should be d/m/yyyy. |
+| `END_DATE` | The ending date of the recurring expense.
The date format should be d/m/yyyy.
This parameter is optional. |
+
+
+
+**:information_source: Info**
+
+Note that once a recurring expense is added, it automatically adds a series of expenses to the expense list at the specified interval until the `END_DATE`. If an `END_DATE` is not yet specified, the expenses will be added up to the current date.
+
+
+
+
:bulb: Tip:
+
+FastTrack's recurring expense feature is designed to help you keep track of regular expenses that occur at a fixed interval, such as monthly subscription fees or cloud storage bills.
+By setting up a recurring expense, you save precious time and effort by automating the process of adding these expenses to FastTrack.
+
+
+
+
+
+**:exclamation: Caution**
+
+Avoid setting an `END_DATE` that is too far in the future or a `START_DATE` that is too far in the past. Setting a date range that spans a large number of years or generates a large number of expenses may cause FastTrack to become temporarily unresponsive.
+For example, if the current date is `3/2/2023` and the `START_DATE` is set to `3/2/2000` with an `INTERVAL` of `day`, this will generate over 8,000 expenses and may cause performance issues.
+
+
+
+### Examples
+
+- `addrec n/milk c/groceries p/4.50 sd/20/3/2023 t/month`
+- `addrec n/milk c/groceries p/4.50 sd/20/3/2023 ed/15/5/2023 t/w`
+
+### Demonstration
+
+1. Enter the command `addrec n/milk c/groceries p/4.50 sd/20/1/2023 t/week` to create a weekly recurring expense starting on 20/1/2023.
+2. FastTrack creates the new recurring expense with the confirmation message `New recurring expense added: Recurring Expense: milk, Amount: 4.5, Category: groceries, Start Date: 2023-01-20, End Date: Ongoing, Recurring Expense Type: WEEKLY`
+
+![FastTrack addrec1](images/demo/recurring_expense/addrec1.png)
+
+3. Enter the command `list` to switch to the **Expense Display**. Notice that FastTrack has automatically added the weekly expenses in the expense list!
+
+![FastTrack addrec2](images/demo/recurring_expense/addrec2.png)
+
+## **Deleting a recurring expense** `delrec`
+
+Deletes an expense category at the specified `INDEX` in the recurring expense list.
+
+Format: `delrec INDEX`
+
+| Parameter | Description |
+| --------- | ------------------------------------------------------------------------------------------------------------------------ |
+| `INDEX` | The index number shown in the displayed recurring expense list.
It must be a positive integer i.e. 1, 2, 3, ... |
+
+
:information_source: Automatic Deletion of Recurring Expenses:
+
+If a recurring expense's end date has already passed, FastTrack automatically deletes the recurring expense the next time the application is started. This means you do not need to worry about manually deleting recurring expenses which are no longer applicable!
+
+
+
+### Examples
+
+- `lrec` followed by `delrec 2` deletes the second recurring expense in the recurring expense list
+- `lrec` followed by `delrec 1` deletes the first recurring expense in the recurring expense list
+
+### Demonstration
+
+1. Enter the command `lrec` to switch to the **Recurring Expense Display**
+2. Enter the command `delrec 2`
+3. FastTrack deletes the second recurring expense in the recurring expense list with the confirmation message `Deleted recurring expense: Recurring Expense: Netflix, Amount: 16, Category: entertainment, Start Date: 2023-01-01, End Date: Ongoing, Recurring Expense Type: MONTHLY`.
+
+![FastTrack delrec](images/demo/recurring_expense/delrec.png)
+
+## **Editing a recurring expense** `edrec`
+
+Edits the expense at the specified `INDEX`
+
+Format: `edrec INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [p/PRICE] [t/INTERVAL] [ed/END_DATE]`
+
+Every parameter except for `INDEX` is optional by themselves, but **at least** one of them must be specified in addition
+to `INDEX`, otherwise the command will not go through.
+
+| Parameter | Description |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `INDEX` | The index of the recurring expense to be edited.
It must be a positive integer i.e. 1, 2, 3, ... |
+| `CATEGORY_NAME` | The new category name of the recurring expense to be changed to.
This parameter is optional. |
+| `EXPENSE_NAME` | The new expense name of the recurring expense to be changed to.
This parameter is optional. |
+| `PRICE` | The new price of the recurring expense to be changed to.
The specified price should be a number, e.g. 4, 4.50.
This parameter is optional. |
+| `INTERVAL` | The new recurrence period of the expense to be changed.
The timeframes available are:
1. `day`
2. `week`
3. `month`
4. `year`
This parameter is optional. |
+| `END_DATE` | The new ending date of recurring expense.
The date format should be d/m/yyyy.
This parameter is optional. |
+
+
+
+**:exclamation: Caution**
+
+If you want to stop a recurring expense before its intended `END_DATE`, make sure to delete it before the current date.
+If you edit the recurring expense to end before the current date, this only prevents new expenses from being added, but expenses that were previously generated will still exist in FastTrack.
+
+
+
+### Examples
+
+- `edrec 1 c/groceries t/week` updates the category and recurrence period first recurring expense in the expense tracker.
+- `edrec 2 p/4.50 ed/15/5/2023` updates the price and ending date of the second recurring expense in the expense tracker.
+
+### Demonstration
+
+1. Enter the command `lrec` to switch to the **Recurring Expense Display**.
+2. Say you have upgraded to a Netflix yearly subscription plan - enter the command `edrec 2 p/200 t/year` .
+3. FastTrack edits the second recurring expense in the recurring expense list with the confirmation message `Edited recurring expense generator: Recurring Expense: Netflix, Amount: 200.0, Category: entertainment, Start Date: 2023-01-20, End Date: Ongoing, Recurring Expense Type: YEARLY`.
+
+![FastTrack edrec1](images/demo/recurring_expense/edrec1.png)
+![FastTrack edrec2](images/demo/recurring_expense/edrec2.png)
+
+
+ Back to Top
+
+
+---
+
+# General Features
+
+### Command Summary
+
+| Feature | Command Format | Examples |
+| --------------------------------------------- | -------------- | ------------ |
+| [**Set Budget**](#setting-a-budget-set) | `set p/AMOUNT` | `set p/1000` |
+| [**Help**](#viewing-help-help) | `help` | `help` |
+| [**Exit program**](#exiting-fasttrack-exit) | `exit` | `exit` |
+| [**Clear data**](#clearing-all-entries-clear) | `CLEAR` | `CLEAR` |
+
+---
+
+## **Setting A Budget** `set`
+
+
+Sets a monthly budget for FastTrack. For first-time users of FastTrack, no budget is set and expense statistics are not updated.
+
+
+In order to view all the expense statistics, you must first set a budget using this command.
+
+FastTrack derives the weekly budget from this monthly budget by dividing the monthly budget by 4.
+
+Format `set p/AMOUNT`
+
+| Parameter | Description |
+| --------- | ---------------------------------------------------------------------------------------- |
+| `AMOUNT` | The monthly budget amount to set. The specified budget should be a number, e.g. 4, 4.50. |
+
+
-### Exiting the program : `exit`
+**:exclamation: Caution**
-Exits the program.
+FastTrack does not allow setting a budget of $0.
+
+
+
+### Examples
+
+* `set p/500` sets the monthly budget of FastTrack to $500.
+
+### Demonstration
+1. Enter the command `set p/500` to set the monthly budget of FastTrack to $500.
+2. FastTrack updates the monthly budget to $500 with the confirmation message `Monthly budget successfully set to $500.0`
+
+![FastTrack set](images/demo/general/set.png)
+
+## **Category Autocompletion**
+
+FastTrack offers a powerful and time-saving feature that autocompletes your category names for you! When you start typing `c/`, FastTrack provides a list of suggested category names as a popup above the command box.
+Give it a try and see how much time you can save with this feature!
+
+### How to Use Category Autocompletion?
+
+- Type `c/` in the command box to trigger the autocompletion feature.
+- FastTrack will display a list of suggested category names above the command box.
+- To select a category name from the list, use the `UP` and `DOWN` arrow keys to navigate through the suggestions list.
+- Press `ENTER` to autocomplete the selected category name.
+- If the category you're looking for is conveniently at the bottom of the list, simply press `TAB` to autocomplete the first suggested category without having to navigate through the list manually!
+- If you decide not to use any of the suggested categories, just continue typing your own category name as per normal.
+
+
:bulb: Tip:
+
+Want to use `TAB` directly without navigating inside the suggestion list?
+Narrow down the list of suggested categories by typing the first few words of your desired category name. Once the option appears at the bottom of the list, simply press `TAB` for autocompletion.
+
+
+
+
+**:exclamation: Caution**
+
+
+To use category autocompletion, make sure that `c/` is the last text you've entered into the command box.
+If there is any other text in front of `c/`, the autocompletion feature will be disabled.
+
+
+
+
+### Demonstration
+
+
+1. Enter `list c/` into the command box.
+2. A list of suggested categories appear in a popup above the command box.
+3. Navigate into the suggestion list using the `UP` arrow key and press `ENTER` on the desired category `Transportation`.
+4. This autocompletes the category name.
+5. If you need to navigate out of the suggestion list, press the `DOWN` arrow key until the cursor returns to the command box.
+
+
+![FastTrack autocomplete_a1](images/demo/general/autocomplete_a1.png)
+![FastTrack autocomplete_a2](images/demo/general/autocomplete_a2.png)
+
+1. Enter `list c/` into the command box.
+2. A list of suggested categories appear in a popup above the command box.
+3. If the desired category `Shopping` is the first suggestion in the list (the bottom-most suggestion), press `TAB` within the command box.
+4. This autocompletes the category name.
+
+![FastTrack autocomplete_b1](images/demo/general/autocomplete_b1.png)
+![FastTrack autocomplete_b2](images/demo/general/autocomplete_b2.png)
+
+## **Clearing all entries** `CLEAR`
+
+Clears all entries from FastTrack. This command removes all stored expenses, recurring expenses and categories.
+
+Format: `CLEAR`
+
+
+**:exclamation: Caution**
+
+This command will delete **all** the data stored in FastTrack apart from the stored monthly budget. To minimise the risk of accidentally using this command, we have made it such that the command only works when the word `CLEAR` is fully uppercase.
+
+Exercise caution before using this command.
+
+
+
+### Demonstration
+
+1. Enter `CLEAR` in the command box.
+2. FastTrack clears all previously logged expenses, recurring expenses and categories, with the confirmation message `Deleted all prior entries`.
+
+![FastTrack clear](images/demo/general/clear.png)
+
+## **Exiting FastTrack** `exit`
+
+After logging your expenses, you might want to close the application and ensure your data is saved.
+This command closes FastTrack and saves the data to the `fastTrack.json` file located on computer's hard disk.
Format: `exit`
-### Saving the data
+## **Viewing help** `help`
+
+Shows a message explaining how to access the help page, as well as a quick rundown of what commands can be used.
+
+Format: `help`
+
+![help message](images/helpMessage.png)
+
+
+ Back to Top
+
-AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually.
+# Expense Statistics Feature
-### Editing the data file
+FastTrack provides you with real-time statistics on your spending to help you keep track of your monthly budget.
+Here are the types of statistics displayed and what they mean.
-AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
+![FastTrack expense_statistic](images/demo/general/summary.png)
+
+## Monthly spending statistic
+
+This statistic represents the **total amount of money you have spent** in the **current month**.
+It includes all expenses recorded in the current month.
+
+
+For example, if the current month is March, this statistic shows the total amount of money spent in March.
+
+## Monthly remaining statistic
+
+This statistic represents the **amount of money you have left** from your **monthly** budget.
+It gives you an idea of how much money you have left to spend for the rest of the month.
+
+## Monthly percentage change statistic
+
+
+This statistic represents the percentage **increase** or **decrease** in your **monthly** spending relative to the previous month.
+The indicator colour is **red** if it is a percentage increase and **green** if it is a percentage decrease.
+
+
+For example, if you spent $500 last month and $750 this month, the monthly percentage change indicator would be `+50.00%` and be displayed in a red colour.
+If you spent $750 last month and $500 this month, the monthly percentage change would be `-33.30%` and be displayed in a green color.
+
+## Weekly spending statistic
+
+
+This statistic represents the **total amount of money you have spent** in the **current week**, starting from Monday to Sunday.
+This gives you an idea of how much money you are spending on a weekly basis.
+
+## Weekly remaining statistic
+
+This statistic represents the **amount of money you have left** from your **weekly** budget.
+Your weekly budget is the value of your monthly budget divided by four. This gives you an idea of how much money you have left to spend for the rest of the week.
:exclamation: **Caution:**
-If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run.
+
+
+Please take note that this value should be treated as a rough guide.
+Even if you have exceeded your previous week's budget, this statistic will show that you have more remaining, as the weekly budget is fixed based on the monthly budget.
+
+Therefore, it is important to use this value as an estimate and not solely rely on it for your spending decisions!
+
+
+
+## Weekly percentage change statistic
+
+This statistic represents the percentage **increase** or **decrease** in your **weekly** spending relative to the previous week.
+The indicator colour is **red** if it is a percentage increase and **green** if it is a percentage decrease.
+
+For example, if you spent $500 last week and $750 this week, the weekly percentage change indicator would be `+50.00%` and be displayed in a red colour.
+If you spent $750 last week and $500 this week, the weekly percentage change would be `-33.30%`.
+
+## Total spent statistic
+
+This statistic represents the **total amount of money you have spent to date**, starting from the first expense you recorded in FastTrack.
+
+This gives you an idea of how much money you have spent over the period of time from when you started tracking your expenses.
+
+## Budget utilisation percentage statistic
+
+This statistic represents the percentage of your monthly budget that you have already utilised in the current month.
+
+For example, if your monthly budget is $1000, and you have already spent $500, your budget utilised percentage would be `50%`.
+This gives you an idea of how much of your monthly budget you have used up.
+
+
+
+**:exclamation: Caution**
+
+Even if you have exceeded your budget, this statistic will reflect that you have fully utilised your budget, and will remain at `100%`.
+
-### Archiving data files `[coming in v2.0]`
+
+ Back to Top
+
-_Details coming soon ..._
+---
---------------------------------------------------------------------------------------------------------------------
+# Saving the data
-## FAQ
+All data in FastTrack are saved in the hard disk automatically after any command that changes the data. There is no need to save manually.
+
+# Editing the data file
+
+FastTrack's data are saved as a JSON file `[JAR file location]/data/fastTrack.json`. Advanced users who are familiar with JSON (JavaScript Object Notation) are welcome to update data directly by editing that data file.
+
+
:exclamation: **Caution:**
+If your changes to the data file makes its format invalid, FastTrack will discard all data and start with an empty data file at the next run.
+
+
+---
+
+# Frequently Asked Questions
**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder.
+**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous FastTrack home folder.
+
+**Q**: Does FastTrack require a Wi-Fi network?
+**A**: No, FastTrack does not need any sort of internet connection to run! You can be rest assured that your data is kept safe locally on your computer. However, accessing the user guide and developer guide which are hosted online will require an internet connection.
+
+**Q**: Why are some of my expenses being categorised as `Misc`?
+**A**: `Misc` is an internal default category in FastTrack that represents an unclassified expense. If you see an expense with the `Misc` category, chances are, the category it was previously associated with was deleted.
+
+**Q**: My expense name gets cut off with trailing ellipses `...`, how do I fix this?
+**A**: Try resizing the FastTrack window size by increasing its width until the full expense names are within view.
---------------------------------------------------------------------------------------------------------------------
+**Q**: Can I set reminders for recurring expenses in FastTrack?
+**A**: No, FastTrack does not currently have a built-in feature for setting reminders for recurring expenses.
+However, you can use an external calendar or reminder app to keep track of recurring expenses.
-## Command summary
+**Q**: Does FastTrack integrate with payment systems like credit cards or PayPal?
+**A**: Currently, FastTrack does not support integration with external payment systems. However, we are constantly improving and expanding our features, and we plan to explore integrating with popular payment systems in the future. Stay tuned for more updates!
-Action | Format, Examples
---------|------------------
-**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague`
-**Clear** | `clear`
-**Delete** | `delete INDEX`
e.g., `delete 3`
-**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
e.g.,`edit 2 n/James Lee e/jameslee@example.com`
-**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake`
-**List** | `list`
-**Help** | `help`
+
+ Back to Top
+
diff --git a/docs/_config.yml b/docs/_config.yml
index 6bd245d8f4e..7bdc137812d 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -1,4 +1,4 @@
-title: "AB-3"
+title: "FastTrack"
theme: minima
header_pages:
@@ -8,7 +8,7 @@ header_pages:
markdown: kramdown
-repository: "se-edu/addressbook-level3"
+repository: "AY2223S2-CS2103T-W09-2/tp"
github_icon: "images/github-icon.png"
plugins:
diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss
index 0d3f6e80ced..47f0b52f2ba 100644
--- a/docs/_sass/minima/_base.scss
+++ b/docs/_sass/minima/_base.scss
@@ -288,7 +288,7 @@ table {
text-align: center;
}
.site-header:before {
- content: "AB-3";
+ content: "FastTrack";
font-size: 32px;
}
}
diff --git a/docs/_sass/minima/skins/classic.scss b/docs/_sass/minima/skins/classic.scss
index 37ea9c5244c..dad5c7cd3a7 100644
--- a/docs/_sass/minima/skins/classic.scss
+++ b/docs/_sass/minima/skins/classic.scss
@@ -6,7 +6,7 @@ $brand-color-dark: darken($brand-color, 25%) !default;
$text-color: #111 !default;
$background-color: #fdfdfd !default;
-$code-background-color: #eef !default;
+$code-background-color: #eff !default;
$link-base-color: #2a7ae2 !default;
$link-visited-color: darken($link-base-color, 15%) !default;
diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml
index 4c5cf58212e..78503252488 100644
--- a/docs/diagrams/ArchitectureDiagram.puml
+++ b/docs/diagrams/ArchitectureDiagram.puml
@@ -9,6 +9,7 @@ Package " "<
>{
Class Logic LOGIC_COLOR
Class Storage STORAGE_COLOR
Class Model MODEL_COLOR
+ Class AnalyticModel MODEL_COLOR
Class Main #grey
Class Commons LOGIC_COLOR_T2
}
@@ -19,6 +20,7 @@ Class "<$documents>" as File UI_COLOR_T1
UI -[#green]> Logic
UI -right[#green]-> Model
+UI -right[#green]-> AnalyticModel
Logic -[#blue]-> Storage
Logic -down[#blue]-> Model
Main -[#grey]-> UI
@@ -28,6 +30,7 @@ Main -up[#grey]-> Model
Main -down[hidden]-> Commons
Storage -up[STORAGE_COLOR].> Model
+Storage -up[STORAGE_COLOR].-> AnalyticModel
Storage .right[STORAGE_COLOR].>File
User ..> UI
@enduml
diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml
index ef81d18c337..b4027a54b59 100644
--- a/docs/diagrams/ArchitectureSequenceDiagram.puml
+++ b/docs/diagrams/ArchitectureSequenceDiagram.puml
@@ -4,7 +4,8 @@
Actor User as user USER_COLOR
Participant ":UI" as ui UI_COLOR
Participant ":Logic" as logic LOGIC_COLOR
-Participant ":Model" as model MODEL_COLOR
+Participant ":Model" as dataModel MODEL_COLOR
+Participant ":AnalyticModel" as analytic MODEL_COLOR
Participant ":Storage" as storage STORAGE_COLOR
user -[USER_COLOR]> ui : "delete 1"
@@ -13,13 +14,22 @@ activate ui UI_COLOR
ui -[UI_COLOR]> logic : execute("delete 1")
activate logic LOGIC_COLOR
-logic -[LOGIC_COLOR]> model : deletePerson(p)
-activate model MODEL_COLOR
+logic -[LOGIC_COLOR]> dataModel : deleteExpense(e)
+activate dataModel MODEL_COLOR
-model -[MODEL_COLOR]-> logic
-deactivate model
+dataModel -[MODEL_COLOR]> analytic : updateAllStatistics()
+activate analytic MODEL_COLOR
+ref over analytic
+ Update statistics in UI
+end ref
+return
+deactivate analytic
-logic -[LOGIC_COLOR]> storage : saveAddressBook(addressBook)
+
+dataModel -[MODEL_COLOR]-> logic
+deactivate dataModel
+
+logic -[LOGIC_COLOR]> storage : saveExpenseTracker(expenseTracker)
activate storage STORAGE_COLOR
storage -[STORAGE_COLOR]> storage : Save to file
diff --git a/docs/diagrams/ComponentManagers.puml b/docs/diagrams/ComponentManagers.puml
index 5e907dc1115..12080856630 100644
--- a/docs/diagrams/ComponentManagers.puml
+++ b/docs/diagrams/ComponentManagers.puml
@@ -5,27 +5,27 @@ skinparam arrowColor LOGIC_COLOR_T4
skinparam classBackgroundColor LOGIC_COLOR
package Logic {
-Class "<>\nLogic" as Logic
+Class "<>\nLogic" as dataLogic
Class LogicManager
}
package Model{
-Class "<>\nModel" as Model
+Class "<>\nModel" as dataModel
Class ModelManager
}
package Storage{
-Class "<>\nStorage" as Storage
+Class "<>\nStorage" as dataStorage
Class StorageManager
}
Class HiddenOutside #FFFFFF
HiddenOutside ..> Logic
-LogicManager .up.|> Logic
-ModelManager .up.|> Model
-StorageManager .up.|> Storage
+LogicManager .up.|> dataLogic
+ModelManager .up.|> dataModel
+StorageManager .up.|> dataStorage
-LogicManager --> Model
-LogicManager --> Storage
+LogicManager --> dataModel
+LogicManager --> dataStorage
@enduml
diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml
index 1dc2311b245..ae27fa2d861 100644
--- a/docs/diagrams/DeleteSequenceDiagram.puml
+++ b/docs/diagrams/DeleteSequenceDiagram.puml
@@ -3,9 +3,9 @@
box Logic LOGIC_COLOR_T1
participant ":LogicManager" as LogicManager LOGIC_COLOR
-participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR
-participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR
-participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR
+participant ":ExpenseTrackerParser" as AddressBookParser LOGIC_COLOR
+participant ":DeleteExpenseCommandParser" as DeleteCommandParser LOGIC_COLOR
+participant "d:DeleteExpenseCommand" as DeleteCommand LOGIC_COLOR
participant ":CommandResult" as CommandResult LOGIC_COLOR
end box
@@ -48,7 +48,7 @@ deactivate AddressBookParser
LogicManager -> DeleteCommand : execute()
activate DeleteCommand
-DeleteCommand -> Model : deletePerson(1)
+DeleteCommand -> Model : deleteExpense(e)
activate Model
Model --> DeleteCommand
diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml
index d4193173e18..07c3c7dc045 100644
--- a/docs/diagrams/LogicClassDiagram.puml
+++ b/docs/diagrams/LogicClassDiagram.puml
@@ -6,13 +6,13 @@ skinparam classBackgroundColor LOGIC_COLOR
package Logic {
-Class AddressBookParser
+Class ExpenseTrackerParser
Class XYZCommand
Class CommandResult
Class "{abstract}\nCommand" as Command
-Class "<>\nLogic" as Logic
+Class "<>\nLogic" as dataLogic
Class LogicManager
}
@@ -26,9 +26,9 @@ package Storage{
Class HiddenOutside #FFFFFF
HiddenOutside ..> Logic
-LogicManager .right.|> Logic
-LogicManager -right->"1" AddressBookParser
-AddressBookParser ..> XYZCommand : creates >
+LogicManager .right.|> dataLogic
+LogicManager -right->"1" ExpenseTrackerParser
+ExpenseTrackerParser ..> XYZCommand : creates >
XYZCommand -up-|> Command
LogicManager .left.> Command : executes >
@@ -38,9 +38,9 @@ LogicManager --> Storage
Storage --[hidden] Model
Command .[hidden]up.> Storage
Command .right.> Model
-note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc
+note right of XYZCommand: XYZCommand = AddExpenseCommand, \nFindCommand, etc
-Logic ..> CommandResult
+dataLogic ..> CommandResult
LogicManager .down.> CommandResult
Command .up.> CommandResult : produces >
@enduml
diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml
index 4439108973a..d623d45f6e1 100644
--- a/docs/diagrams/ModelClassDiagram.puml
+++ b/docs/diagrams/ModelClassDiagram.puml
@@ -5,46 +5,41 @@ skinparam arrowColor MODEL_COLOR
skinparam classBackgroundColor MODEL_COLOR
Package Model <>{
-Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook
+Class "<>\nReadOnlyExpenseTracker" as ReadOnlyExpenseTracker
Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs
-Class "<>\nModel" as Model
-Class AddressBook
+Class "<>\nModel" as dataModel
+Class ExpenseTracker
Class ModelManager
Class UserPrefs
-Class UniquePersonList
-Class Person
-Class Address
-Class Email
-Class Name
-Class Phone
-Class Tag
-
+Class UniqueCategoryList
+Class ExpenseList
+Class RecurringExpenseList
+Class Category
+Class Expense
+Class RecurringExpenseManager
}
Class HiddenOutside #FFFFFF
HiddenOutside ..> Model
-AddressBook .up.|> ReadOnlyAddressBook
+ExpenseTracker .up.|> ReadOnlyExpenseTracker
-ModelManager .up.|> Model
-Model .right.> ReadOnlyUserPrefs
-Model .left.> ReadOnlyAddressBook
-ModelManager -left-> "1" AddressBook
+ModelManager .up.|> dataModel
+dataModel .right.> ReadOnlyUserPrefs
+dataModel .left.> ReadOnlyExpenseTracker
+ModelManager -down-> "1" ExpenseTracker
ModelManager -right-> "1" UserPrefs
UserPrefs .up.|> ReadOnlyUserPrefs
-AddressBook *--> "1" UniquePersonList
-UniquePersonList --> "~* all" Person
-Person *--> Name
-Person *--> Phone
-Person *--> Email
-Person *--> Address
-Person *--> "*" Tag
-
-Name -[hidden]right-> Phone
-Phone -[hidden]right-> Address
-Address -[hidden]right-> Email
+ExpenseTracker *--> "1" UniqueCategoryList
+ExpenseTracker *--> "1" ExpenseList
+ExpenseTracker *--> "1" RecurringExpenseList
+UniqueCategoryList --> "~* all" Category
+ExpenseList --> "~* all" Expense
+RecurringExpenseList --> "~* all" RecurringExpenseManager
-ModelManager -->"~* filtered" Person
+ModelManager -->"~* filtered" Category
+ModelManager -->"~* filtered" Expense
+ModelManager -->"~* filtered" RecurringExpenseManager
@enduml
diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml
index 0c7424de6e0..09fbe9bee88 100644
--- a/docs/diagrams/ParserClasses.puml
+++ b/docs/diagrams/ParserClasses.puml
@@ -9,7 +9,7 @@ Class XYZCommand
package "Parser classes"{
Class "<>\nParser" as Parser
-Class AddressBookParser
+Class ExpenseTrackerParser
Class XYZCommandParser
Class CliSyntax
Class ParserUtil
@@ -19,12 +19,12 @@ Class Prefix
}
Class HiddenOutside #FFFFFF
-HiddenOutside ..> AddressBookParser
+HiddenOutside ..> ExpenseTrackerParser
-AddressBookParser .down.> XYZCommandParser: creates >
+ExpenseTrackerParser .down.> XYZCommandParser: creates >
XYZCommandParser ..> XYZCommand : creates >
-AddressBookParser ..> Command : returns >
+ExpenseTrackerParser ..> Command : returns >
XYZCommandParser .up.|> Parser
XYZCommandParser ..> ArgumentMultimap
XYZCommandParser ..> ArgumentTokenizer
diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml
index 760305e0e58..5ca0d26a0b4 100644
--- a/docs/diagrams/StorageClassDiagram.puml
+++ b/docs/diagrams/StorageClassDiagram.puml
@@ -11,15 +11,17 @@ Class "<>\nUserPrefsStorage" as UserPrefsStorage
Class JsonUserPrefsStorage
}
-Class "<>\nStorage" as Storage
+Class "<>\nStorage" as dataStorage
Class StorageManager
-package "AddressBook Storage" #F4F6F6{
-Class "<>\nAddressBookStorage" as AddressBookStorage
-Class JsonAddressBookStorage
-Class JsonSerializableAddressBook
-Class JsonAdaptedPerson
-Class JsonAdaptedTag
+package "ExpenseTracker Storage" #F4F6F6{
+Class "<>\nExpenseTrackerStorage" as ExpenseTrackerStorage
+Class JsonExpenseTrackerStorage
+Class JsonSerializableExpenseTracker
+Class JsonAdaptedExpense
+Class JsonAdaptedCategory
+Class JsonAdaptedRecurringExpense
+Class JsonAdaptedBudget
}
}
@@ -27,17 +29,19 @@ Class JsonAdaptedTag
Class HiddenOutside #FFFFFF
HiddenOutside ..> Storage
-StorageManager .up.|> Storage
+StorageManager .up.|> dataStorage
StorageManager -up-> "1" UserPrefsStorage
-StorageManager -up-> "1" AddressBookStorage
+StorageManager -up-> "1" ExpenseTrackerStorage
-Storage -left-|> UserPrefsStorage
-Storage -right-|> AddressBookStorage
+dataStorage -left-|> UserPrefsStorage
+dataStorage -right-|> ExpenseTrackerStorage
JsonUserPrefsStorage .up.|> UserPrefsStorage
-JsonAddressBookStorage .up.|> AddressBookStorage
-JsonAddressBookStorage ..> JsonSerializableAddressBook
-JsonSerializableAddressBook --> "*" JsonAdaptedPerson
-JsonAdaptedPerson --> "*" JsonAdaptedTag
+JsonExpenseTrackerStorage .up.|> ExpenseTrackerStorage
+JsonExpenseTrackerStorage ..> JsonSerializableExpenseTracker
+JsonSerializableExpenseTracker --> "*" JsonAdaptedExpense
+JsonSerializableExpenseTracker --> "*" JsonAdaptedCategory
+JsonSerializableExpenseTracker --> "*" JsonAdaptedRecurringExpense
+JsonSerializableExpenseTracker --> "*" JsonAdaptedBudget
@enduml
diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml
index 95473d5aa19..d5f2d2245cd 100644
--- a/docs/diagrams/UiClassDiagram.puml
+++ b/docs/diagrams/UiClassDiagram.puml
@@ -9,11 +9,20 @@ Class "<>\nUi" as Ui
Class "{abstract}\nUiPart" as UiPart
Class UiManager
Class MainWindow
+Class CategoryCard
+Class ExpenseCard
+Class ExpenseListPanel
+Class RecurringExpenseCard
+Class RecurringExpenseListPanel
Class HelpWindow
Class ResultDisplay
-Class PersonListPanel
-Class PersonCard
+Class CategoryListPanel
+Class SuggestionCard
+Class SuggestionListPanel
+Class StatisticsPanel
Class StatusBarFooter
+Class ResultDetails
+Class ResultHeader
Class CommandBox
}
@@ -21,40 +30,57 @@ package Model <> {
Class HiddenModel #FFFFFF
}
+package AnalyticModel <> {
+Class HiddenModel #FFFFFF
+}
+
package Logic <> {
Class HiddenLogic #FFFFFF
}
Class HiddenOutside #FFFFFF
-HiddenOutside ..> Ui
UiManager .left.|> Ui
-UiManager -down-> "1" MainWindow
+UiManager -> "1" MainWindow
MainWindow *-down-> "1" CommandBox
+MainWindow *-right> "1" StatisticsPanel
MainWindow *-down-> "1" ResultDisplay
-MainWindow *-down-> "1" PersonListPanel
+MainWindow *-down-> "1" ExpenseListPanel
+MainWindow *-down-> "1" CategoryListPanel
+MainWindow *-down-> "1" RecurringExpenseListPanel
MainWindow *-down-> "1" StatusBarFooter
-MainWindow --> "0..1" HelpWindow
+MainWindow *-down-> "1" ResultHeader
+MainWindow *-down-> "1" ResultDetails
+MainWindow *-down-> "1" SuggestionListPanel
+MainWindow -down-> "0..1" HelpWindow
-PersonListPanel -down-> "*" PersonCard
+CategoryListPanel -down-> "*" CategoryCard
+ExpenseListPanel -down-> "*" ExpenseCard
+RecurringExpenseListPanel -down-> "*" RecurringExpenseCard
+SuggestionListPanel -down-> "*" SuggestionCard
MainWindow -left-|> UiPart
ResultDisplay --|> UiPart
CommandBox --|> UiPart
-PersonListPanel --|> UiPart
-PersonCard --|> UiPart
StatusBarFooter --|> UiPart
HelpWindow --|> UiPart
+CategoryListPanel --|> UiPart
+CategoryCard --|> UiPart
+ExpenseListPanel --|> UiPart
+ExpenseCard --|> UiPart
+RecurringExpenseListPanel --|> UiPart
+RecurringExpenseCard --|> UiPart
+SuggestionListPanel --|> UiPart
+SuggestionCard --|> UiPart
-PersonCard ..> Model
-UiManager -right-> Logic
-MainWindow -left-> Logic
+UiManager -up-> Logic
+MainWindow -up-> Logic
+StatisticsPanel -up-> AnalyticModel
+AnalyticModel -right-> Model
-PersonListPanel -[hidden]left- HelpWindow
HelpWindow -[hidden]left- CommandBox
-CommandBox -[hidden]left- ResultDisplay
-ResultDisplay -[hidden]left- StatusBarFooter
+ResultHeader -[hidden]left- ResultDisplay
MainWindow -[hidden]-|> UiPart
@enduml
diff --git a/docs/diagrams/activity_diagrams/addRecurringExpenseActivityDiagram.puml b/docs/diagrams/activity_diagrams/addRecurringExpenseActivityDiagram.puml
new file mode 100644
index 00000000000..7f72164fbb3
--- /dev/null
+++ b/docs/diagrams/activity_diagrams/addRecurringExpenseActivityDiagram.puml
@@ -0,0 +1,31 @@
+@startuml
+start
+:User executes "addrec" command;
+:"addrec" command is parsed;
+
+if () then ([arguments present & in valid format])
+ if () then ([end date later than start date])
+ :RecurringExpenseManager object created;
+ :AddRecurringExpenseCommand object created and executed;
+ if () then ([recurring expense is unique)
+ :Search category list for instance matching category name;
+ if () then ([matching instance found])
+ :Link matching category to RecurringExpenseManager;
+ else ([else])
+ :Add category to category list;
+ endif;
+ :Add RecurringExpenseManager object to recurring expense list;
+ :Generate new expenses from recurring expense;
+ :Display success message;
+ else ([else])
+ :Error message displayed specifying duplicate recurring expense;
+ endif
+ else ([else])
+ :Error message displayed specifying end date was earlier than start date;
+ endif
+
+else ([else])
+ :Error message displayed specifying an error in the command format;
+endif
+stop
+@enduml
diff --git a/docs/diagrams/activity_diagrams/deleteRecurringExpenseActivityDiagram.puml b/docs/diagrams/activity_diagrams/deleteRecurringExpenseActivityDiagram.puml
new file mode 100644
index 00000000000..603de8d0947
--- /dev/null
+++ b/docs/diagrams/activity_diagrams/deleteRecurringExpenseActivityDiagram.puml
@@ -0,0 +1,18 @@
+@startuml
+start
+:User executes "delrec" command;
+:"delrec" command is parsed;
+
+if () then ([index is present])
+ if () then ([index is within range])
+ :Get RecurringExpenseManager instance to edit;
+ :Delete RecurringExpenseManager from recurring expense list;
+ :Display success message;
+ else ([else])
+ :Error message displayed specifying index out of range;
+ endif;
+else ([else])
+ :Error message displayed specifying an error in the command format;
+endif
+stop
+@enduml
diff --git a/docs/diagrams/activity_diagrams/editRecurringExpenseActivityDiagram.puml b/docs/diagrams/activity_diagrams/editRecurringExpenseActivityDiagram.puml
new file mode 100644
index 00000000000..1653cc6dd45
--- /dev/null
+++ b/docs/diagrams/activity_diagrams/editRecurringExpenseActivityDiagram.puml
@@ -0,0 +1,24 @@
+@startuml
+start
+:User executes "edrec" command;
+:"edrec" command is parsed;
+
+if () then ([at least one argument is present])
+ if () then ([arguments in valid format and index within range])
+ :Get RecurringExpenseManager instance to edit;
+ :Search category list for instance matching category name;
+ if () then ([matching instance found])
+ :Link matching category to RecurringExpenseManager;
+ :Update necessary attributes in RecurringExpenseManager;
+ :Display success message;
+ else ([else])
+ :Error message displayed specifying nonexistent category;
+ endif;
+ else ([else])
+ :Error message displayed specifying an error in the command format or index out of range;
+ endif
+else ([else])
+ :Error message displayed specifying no attributes given to edit;
+endif
+stop
+@enduml
diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml
index fad8b0adeaa..2a3b44b9858 100644
--- a/docs/diagrams/style.puml
+++ b/docs/diagrams/style.puml
@@ -55,8 +55,9 @@ skinparam Sequence {
MessageAlign center
BoxFontSize 15
BoxPadding 0
- BoxFontColor #FFFFFF
+ BoxFontColor #000000
FontName Arial
+ ReferenceBackgroundColor #FFFFFF
}
skinparam Participant {
diff --git a/docs/diagrams/uml/commands.puml b/docs/diagrams/uml/commands.puml
new file mode 100644
index 00000000000..090bd8252a9
--- /dev/null
+++ b/docs/diagrams/uml/commands.puml
@@ -0,0 +1,221 @@
+@startuml commands
+skinparam defaultTextAlignment left
+' scale 0.6
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+
+interface Command {
+ + abstract execute(dataModel: DataModel): CommandResult
+}
+
+class CommandResult {
+ + ComandResult(feedbackToUser: String, screenType: ScreenType)
+ + CommandResult(feedbackToUser: String, screenType: ScreenType, \
+ showHelp: boolean, exit: boolean)
+ + getFeedbackToUser(): String
+ + getScreenType(): ScreenType
+ + isShowHelp(): boolean
+ + isExit(): boolean
+ - feedbackToUser: String
+ - screenType: ScreenType
+ - showHelp: boolean
+ - exit: boolean
+}
+
+interface AddCommand extends Command {
+}
+
+interface DeleteCommand extends Command {
+}
+
+interface EditCommand extends Command {
+}
+
+interface ListCommand extends Command {
+}
+
+interface GeneralCommand extends Command {
+}
+
+class SetBudgetCommand {
+ + SetBudgetCommand(budget: Budget)
+ - budget: Budget
+}
+
+Command <.. SetBudgetCommand
+Command "1" *-- "1" CommandResult
+
+@enduml
+
+@startuml add_commands
+
+skinparam defaultTextAlignment left
+' scale 0.6
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+' COMMAND_WORD, MESSAGE_USAGE< MESSSAGE_SUCCESS, MESSAGE_DUPLICATE_CATEGORY are not shown
+interface AddCommand {
+ + abstract execute(dataModel: DataModel): CommandResult
+}
+
+class AddCategoryCommand {}
+
+class AddExpenseCommand {
+ + AddExpenseCommand(expense: Expense)
+ - expense: Expense
+}
+
+class AddRecurringExpenseCommand {
+ + AddRecurringExpenseCommand(toAdd: RecurringExpenseManager)
+ - toAdd: RecurringExpenseManager
+}
+
+class CategorySummaryCommand {
+ + CategorySummaryCommand(index: Index)
+ - index: Index
+}
+
+AddCommand <|-- AddCategoryCommand
+AddCommand <|-- AddExpenseCommand
+AddCommand <|-- AddRecurringExpenseCommand
+
+@enduml
+
+@startuml delete_commands
+
+skinparam defaultTextAlignment left
+' scale 0.6
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+interface DeleteCommand {}
+
+class DeleteCategoryCommand {
+ + DeleteCategoryCommand(index: Index)
+ - index: Index
+}
+
+class DeleteExpenseCommand {
+ + DeleteExpenseCommand(index: Index)
+ - index: Index
+}
+
+class DeleteRecurringExpenseCommand {
+ + DeleteRecurringExpenseCommand(index: Index)
+ - index: Index
+}
+
+DeleteCommand <.. DeleteCategoryCommand
+DeleteCommand <.. DeleteExpenseCommand
+DeleteCommand <.. DeleteRecurringExpenseCommand
+
+@enduml
+
+@startuml edit_commands
+
+skinparam defaultTextAlignment left
+' scale 0.6
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+interface EditCommand {}
+
+class EditCategoryCommand {
+ + EditCategoryCommand(index: Index, newCategoryName: String, newSummary: String)
+ - index: Index
+ - newCategoryName: String
+ - newSummary: String
+}
+
+class EditExpenseCommand {
+ ' + EditExpenseCommand(index: Index, newExpenseName: String, newExpenseAmount: Double, \
+ newExpenseDate: LocalDate, newExpenseCategory: Category) '
+ - index: Index
+ - newExpenseName: String
+ - newExpenseAmount: Double
+ - newExpenseDate: LocalDate
+ - newExpenseCategory: Category
+}
+
+class EditRecurringExpenseManagerCommand {
+ ' + EditRecurringExpenseManagerCommand(index: Index, newExpenseName: String, \
+ newExpenseAmount: Double, newExpenseCategoryInString: String, \
+ newExpenseEndDate: LocalDate, newFrequencyInString: String) '
+ - index: Index
+ - newExpenseName: String
+ - newExpenseAmount: Double
+ - newExpenseCategoryInString: String
+ - newExpenseEndDate: LocalDate
+ - newFrequencyInString: String
+}
+
+
+EditCommand <.. EditCategoryCommand
+EditCommand <.. EditExpenseCommand
+EditCommand <.. EditRecurringExpenseManagerCommand
+
+@enduml
+
+@startuml list_commands
+
+skinparam defaultTextAlignment left
+' scale 0.6
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+interface ListCommand {}
+
+class ListCategoryCommand {}
+
+class ListExpensesCommand {
+ ' + ListCategoriesCommand(categoryPredicate: Optional, \
+ timespanPredicate: Optional) '
+ - categoryPredicate: Optional
+ - timespanPredicate: Optional
+}
+
+class ListRecurringExpenseCommand {}
+
+
+ListCommand <.. ListCategoryCommand
+ListCommand <.. ListExpensesCommand
+ListCommand <.. ListRecurringExpenseCommand
+
+@enduml
+
+@startuml general_commands
+
+skinparam defaultTextAlignment left
+' scale 0.6
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+interface GeneralCommand {}
+
+class ClearCommand {}
+
+class ExitCommand {}
+
+class FindCommand {
+ + FindCommand(predicate: ExpenseContainsKeywordsPredicate)
+ - predicate: ExpenseContainsKeywordsPredicate
+}
+
+class HelpCommand {}
+
+GeneralCommand <.. CategorySummaryCommand
+GeneralCommand <.. ClearCommand
+GeneralCommand <.. ExitCommand
+GeneralCommand <.. FindCommand
+GeneralCommand <.. HelpCommand
+
+@enduml
+
diff --git a/docs/diagrams/uml/model.puml b/docs/diagrams/uml/model.puml
new file mode 100644
index 00000000000..d02864fdf93
--- /dev/null
+++ b/docs/diagrams/uml/model.puml
@@ -0,0 +1,227 @@
+@startuml model
+hide empty members
+hide circle
+skinparam classAttributeIconSize 0
+
+class Budget {
+ + Budget(budget: double)
+ + getMonthlyBudget(): double
+ + getWeeklyBudget(): double
+ + setMonthlyBudget(monthBudget: double): void
+ - monthBudget: double
+}
+
+abstract class Category {
+ + Category(categoryName: String, summary: String)
+ + getCategoryName(): String
+ + getSummary(): String
+ + isValidCategoryName(categoryName: String): boolean
+ # categoryName: String
+ # summary: String
+}
+
+class MiscellaneousCategory extends Category {
+ + MiscellaneousCategory()
+}
+
+class UserDefinedCategory extends Category {
+ + UserDefinedCategory(categoryName: String, summary: String)
+ + setCategoryName(categoryName: String): void
+ + setDescription(summary: String): void
+}
+
+class UniqueCategoryList {
+ + contains(category: Category): boolean
+ + add(newCategory: Category): void
+ + remove(category: Category): void
+ + clear(): void
+ + setCategoryList(listOfCategories: List): void
+ + categoriesAreUnique(listOfCategories: List): boolean
+ + asUnmodifiableList(): ObservableList
+ - internalListOfCategories: ObservableList
+ - internalUnmodifiableList: ObservableList
+}
+
+' getters and setters are not shown
+class Expense {
+ + Expense(name: String, amount: Price, date: LocalDate, category: Category)
+ + getFormattedDate(): String
+ + {static} isValidName(name: String): boolean
+ + {static} MESSAGE_CONSTRAINTS: String
+ + {static} VALIDATION_REGEX: String
+ - name: String
+ - amount: Price
+ - date: LocalDate
+ - category: Category
+}
+
+class Price {
+ + Price(amount: String)
+ + getPriceAsDouble(): double
+ + {static} isValidPrice(amount: String): boolean
+ + {static} MESSAGE_CONSTRAINTS: String
+ + {static} VALIDATION_REGEX: String
+ - amount: String
+}
+
+class ExpenseList {
+ + add(newExpense: Expense): void
+ + remove(expense: Expense): void
+ + clear(): void
+ + set(index: int, expense: Expense): void
+ + replaceDeletedCategory(target: Category): void
+ + contains(expense: Expense): boolean
+ + setExpenseList(listOfExpenses: List): void
+ + getSize(): int
+ + getTotalAmount(): double
+ + sortList(): void
+ + asUnmodifiableList(): ObservableList
+ - internalListOfExpenses: ObservableList
+ - internalUnmodifiableList: ObservableList
+ - misc: MiscellaneousCategory
+}
+
+class RecurringExpenseList {
+ + addRecurringExpense(recurringExpense: RecurringExpenseManager): void
+ + removeRecurringExpense(recurringExpense: RecurringExpenseManager): void
+ + getExpenses(): ArrayList
+ + cleanupExpiredGenerators(): void
+ + getSize(): int
+ : getTotalAmount(): double
+ + asUnmodifiableList(): ObservableList
+ - recurringExpenseList: ObservableList
+ - internalUnmodifiableList: ObservableList
+}
+
+class RecurringExpenseManager {
+ + RecurringExpenseManager(expenseName: String, amount: Price, expenseCategory: Category, numberOfExpenses: int, nextExpenseDate: LocalDate, startDate: LocalDate, endDate: LocalDate, recurringExpenseType: RecurringExpenseType)
+ + getExpenses(): ArrayList
+ - expenseName: String
+ - amount: Price
+ - expenseCategory: Category
+ - numberOfExpenses: int
+ - nextExpenseDate: LocalDate
+ - startDate: LocalDate
+ - endDate: LocalDate
+ - recurringExpenseType: RecurringExpenseType
+}
+
+enum RecurringExpenseType {
+ + DAILY
+ + WEEKLY
+ + MONTHLY
+ + YEARLY
+ + getNextExpenseDate(currentDate: LocalDate): LocalDate
+}
+
+class ExpenseContainsKeywordsPredicate {
+ + ExpenseContainsKeywordsPredicate(keywords: List)
+ - keywords: List
+}
+
+class ExpenseInTimespanPredicate {
+ + ExpenseInTimespanPredicate(timespan: Timespan)
+ - timespan: Timespan
+}
+
+class ExpenseInCategoryPredicate {
+ + ExpenseInCategoryPredicate(category: Category)
+ - category: Category
+}
+
+enum AnalyticsType {
+ + MONTHLY_SPENT
+ + WEEKLY_SPENT
+ + TOTAL_SPENT
+ + MONTHLY_REMAINING
+ + WEEKLY_REMAINING
+ + WEEKLY_CHANGE
+ + MONTHLY_CHANGE
+ + BUDGET_PERCENTAGE
+}
+
+interface AnalyticModel {
+ + getAnalyticsData(type: AnalyticsType): double
+ + getMonthlyBudget(): DoubleProperty
+ + getMonthlyRemaining(): DoubleProperty
+ + getWeeklySpent(): DoubleProperty
+ + getWeeklyRemaining(): DoubleProperty
+ + getWeeklyChange(): DoubleProperty
+ + getMonthlyChange(): DoubleProperty
+ + getTotalSpent(): DoubleProperty
+ + getBudgetPercentage(): DoubleProperty
+ + updateMonthlyBudgetProperty(newBudget: Budget): void
+ + updateWeeklyBudgetProperty(newBudget: Budget): void
+ + updateAllStatistics(): void
+}
+
+class AnalyticsModelManager {
+ + AnalyticsModelManager(expenseTracker: ExpenseTracker, referenceDate: LocalDate)
+}
+
+
+interface ReadOnlyExpenseTracker {
+ + getExpenseList(): ObservableList
+ + getRecurringExpenseGenerators(): ObservableList
+ + getCategoryList(): ObservableList
+ + getBudgetForStats(): ObjectProperty
+ + getBudget(): Budget
+}
+
+' add, has, and remove methods are not shown
+class ExpenseTracker {
+ + ExpenseTracker()
+ + ExpenseTracker(toBeCopied: ReadOnlyExpenseTracker)
+ + resetData(newData: ReadOnlyExpenseTracker): void
+ + generateRetroactiveExpenses(): void
+ + cleanUpRecurrenceGenerators(): void
+ - expenses: ExpenseList
+ - recurringGenerators: RecurringExpenseList
+ - categories: UniqueCategoryList
+ - simpleBudget: ObjectProperty
+}
+
+interface ReadOnlyUserPrefs {
+ + getGuiSettings(): GuiSettings
+ + getExpenseTrackerFilePath(): Path
+}
+
+class UserPrefs {
+ + UserPrefs(UserPrefs: : ReadOnlyUserPrefs)
+ + resetData(newUserPrefs: ReadOnlyUserPrefs): void
+ - guiSettings: GuiSettings
+ - expenseTrackerFilePath: Path
+}
+
+ReadOnlyExpenseTracker <|-- ExpenseTracker
+ReadOnlyUserPrefs <|-- UserPrefs
+ExpenseTracker *-- ExpenseList
+ExpenseTracker *-- RecurringExpenseList
+ExpenseTracker *-- UniqueCategoryList
+ExpenseTracker *-- Budget
+ExpenseList *-- Expense
+RecurringExpenseList *-- RecurringExpenseManager
+UniqueCategoryList *-- Category
+Category <|-- MiscellaneousCategory
+Category <|-- UserDefinedCategory
+Expense *-- Price
+Expense *-- Category
+RecurringExpenseManager *-- Price
+RecurringExpenseManager *-- Category
+RecurringExpenseManager *-- RecurringExpenseType
+ExpenseList *-- ExpenseContainsKeywordsPredicate
+ExpenseList *-- ExpenseInTimespanPredicate
+ExpenseList *-- ExpenseInCategoryPredicate
+AnalyticModel <|-- AnalyticsModelManager
+AnalyticsModelManager *-- ExpenseTracker
+AnalyticsModelManager *-- LocalDate
+AnalyticsModelManager *-- Budget
+AnalyticsModelManager *-- AnalyticsType
+AnalyticsModelManager *-- DoubleProperty
+AnalyticsModelManager *-- ObjectProperty
+AnalyticsModelManager *-- ReadOnlyExpenseTracker
+AnalyticsModelManager *-- ReadOnlyUserPrefs
+AnalyticsModelManager *-- GuiSettings
+AnalyticsModelManager *-- Path
+
+@enduml
diff --git a/docs/diagrams/uml/parser.puml b/docs/diagrams/uml/parser.puml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/docs/diagrams/uml/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.puml b/docs/diagrams/uml/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.puml
new file mode 100644
index 00000000000..add7df7af4c
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.puml
@@ -0,0 +1,46 @@
+@startuml
+!include ../../style.puml
+
+participant ":Mainapp" as Main #097969
+
+box Model MODEL_COLOR_T1
+participant "dataModel:Model" as dataModel MODEL_COLOR
+participant "expenseTracker:ExpenseTracker" as ExpenseTracker MODEL_COLOR
+participant ":RecurringExpenseManager" as Manager MODEL_COLOR
+end box
+
+
+create dataModel
+Main -> dataModel
+activate dataModel
+
+create ExpenseTracker
+dataModel -> ExpenseTracker
+activate ExpenseTracker
+
+ExpenseTracker -> ExpenseTracker : resetData()
+activate ExpenseTracker
+ExpenseTracker -> ExpenseTracker : generateRetroactiveExpenses()
+loop for each RecurringExpenseManager
+ activate ExpenseTracker
+ ExpenseTracker -> Manager : getExpenses()
+ activate Manager
+ Manager --> ExpenseTracker : list of recurring expenses
+ deactivate Manager
+ ref over ExpenseTracker
+ add expenses in the list
+ to FastTrack
+ end ref
+end
+ExpenseTracker --> ExpenseTracker
+deactivate ExpenseTracker
+
+ExpenseTracker --> ExpenseTracker
+deactivate ExpenseTracker
+ExpenseTracker --> dataModel
+deactivate ExpenseTracker
+
+Main <-- dataModel
+deactivate dataModel
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/SetBudgetSequenceDiagram.puml b/docs/diagrams/uml/sequence_diagrams/SetBudgetSequenceDiagram.puml
new file mode 100644
index 00000000000..a83aa7e5021
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/SetBudgetSequenceDiagram.puml
@@ -0,0 +1,37 @@
+@startuml
+!include ../../style.puml
+
+box Logic LOGIC_COLOR_T1
+participant ":SetBudgetCommand" as SetBudgetCommand LOGIC_COLOR
+end box
+
+box Model MODEL_COLOR_T1
+participant "dataModel:Model" as Model MODEL_COLOR
+participant "expenseTracker:ExpenseTracker" as ExpenseTracker MODEL_COLOR
+end box
+
+[-> SetBudgetCommand : execute(dataModel)
+activate SetBudgetCommand
+
+SetBudgetCommand -> Model : setBudget(budget)
+activate Model
+
+Model -> ExpenseTracker : setBudget(budget)
+activate ExpenseTracker
+
+ref over ExpenseTracker
+ Set simpleBudget field
+ in ExpenseTracker
+end ref
+
+ExpenseTracker --> Model
+deactivate ExpenseTracker
+
+Model --> SetBudgetCommand
+deactivate Model
+
+[<-- SetBudgetCommand : CommandResult
+deactivate SetBudgetCommand
+destroy SetBudgetCommand
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/add_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/add_expense_command.puml
new file mode 100644
index 00000000000..1243042bc96
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/add_expense_command.puml
@@ -0,0 +1,91 @@
+@startuml AddExpenseSequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+box Model MODEL_COLOR_T1
+participant "newExpense: Expense" as Expense order 2 MODEL_COLOR
+participant "dataModel: Model" as Model order 3 MODEL_COLOR
+participant "expenseTracker: ExpenseTracker" as ExpenseTracker order 4 MODEL_COLOR
+participant "expenses: ExpenseList" as ExpenseList order 5 MODEL_COLOR
+end box
+
+box Logic LOGIC_COLOR_T1
+participant ":AddExpenseCommand" as Command LOGIC_COLOR
+participant ":CommandResult" as Result LOGIC_COLOR
+end box
+
+[->Command: execute(dataModel)
+activate Command
+Command -> Expense: getCategory()
+activate Expense
+Expense --> Command: newCategory
+deactivate Expense
+Command -> Model : getCategoryInstance(newCategory)
+ref over Model, ExpenseTracker
+ get existing category
+end ref
+Model --> Command: existingCategory
+alt existingCategory != null
+ Command -> Expense: setCategory(existingCategory)
+ activate Expense
+ Expense -> Model
+ ref over Model
+ sets category to
+ link to existing one.
+ end ref
+ Model --> Expense
+ Expense --> Command
+ deactivate Expense
+else else
+ Command -> Model
+ activate Model
+ Model -> ExpenseTracker : addCategory(toAdd)
+ activate ExpenseTracker
+
+ ExpenseTracker -> ExpenseList
+ ref over ExpenseList
+ add newCategory to
+ ObservableList
+ end ref
+ ExpenseList --> ExpenseTracker
+ ExpenseTracker --> Model
+ deactivate ExpenseTracker
+ Model --> Command
+ deactivate Model
+end
+Command -> Model: addExpense(newExpense)
+activate Model
+Model -> ExpenseTracker: addExpense(newExpense)
+activate ExpenseTracker
+ExpenseTracker -> ExpenseList: add(expense)
+activate ExpenseList
+ExpenseList -> ExpenseList: add newExpense to \nObservableList
+ExpenseList --> ExpenseTracker
+deactivate ExpenseList
+ExpenseTracker -> ExpenseList: sortList()
+activate ExpenseList
+ExpenseList -> ExpenseList: sort expenses in \nObservableList\nby date
+ExpenseList --> ExpenseTracker
+deactivate ExpenseList
+ExpenseTracker --> Model
+deactivate ExpenseTracker
+Model -> Model: updateFilteredExpenseList\n(PREDICATE_SHOW_ALL_EXPENSES)
+activate Model
+deactivate Model
+Model --> Command
+deactivate Model
+create Result
+Command -> Result
+activate Result
+Result --> Command
+deactivate Result
+[<-- Command: result
+destroy Command
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/delete_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/delete_expense_command.puml
new file mode 100644
index 00000000000..bacf1680dab
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/delete_expense_command.puml
@@ -0,0 +1,65 @@
+@startuml DeleteExpenseSequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+box Model MODEL_COLOR_T1
+participant "dataModel: Model" as Model order 3 MODEL_COLOR
+participant "expenseTracker: ExpenseTracker" as ExpenseTracker order 4 MODEL_COLOR
+participant "expenses: ExpenseList" as ExpenseList order 5 MODEL_COLOR
+end box
+
+box Logic LOGIC_COLOR_T1
+participant ":DeleteExpenseCommand" as Command LOGIC_COLOR
+participant ":CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+[->Command: execute(dataModel)
+activate Command
+Command -> Model: getFilteredExpenseList()
+activate Model
+Model --> Command: lastShownList
+deactivate Model
+
+Command -> Model
+ref over Model
+ get expense from
+ lastShownList using parsed index
+end ref
+Model --> Command
+
+Command -> Model: deleteExpense(expense)
+activate Model
+Model -> ExpenseTracker: deleteExpense(expense)
+activate ExpenseTracker
+ExpenseTracker -> ExpenseList: remove(expense)
+activate ExpenseList
+ExpenseList -> ExpenseList: remove expense from \nObservableList
+ExpenseList --> ExpenseTracker
+deactivate ExpenseList
+ExpenseTracker -> ExpenseList: sortList()
+activate ExpenseList
+ExpenseList -> ExpenseList: sort expenses in \nObservableList\nby date
+ExpenseList --> ExpenseTracker
+deactivate ExpenseList
+ExpenseTracker --> Model
+deactivate ExpenseTracker
+Model -> Model: updateFilteredExpenseList\n(PREDICATE_SHOW_ALL_EXPENSES)
+activate Model
+deactivate Model
+Model --> Command
+deactivate Model
+create CommandResult
+Command -> CommandResult
+activate CommandResult
+CommandResult --> Command
+deactivate CommandResult
+[<-- Command: result
+destroy Command
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/edit_exense_command_part.puml b/docs/diagrams/uml/sequence_diagrams/edit_exense_command_part.puml
new file mode 100644
index 00000000000..1a1cb9c4729
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/edit_exense_command_part.puml
@@ -0,0 +1,35 @@
+@startuml EditExpenseFindPartSequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+box Logic LOGIC_COLOR_T1
+participant ":EditExpenseCommand" as Command order 1 LOGIC_COLOR
+end box
+
+box Model MODEL_COLOR_T1
+participant "category: Category" as Category order 2 MODEL_COLOR
+end box
+
+loop until matching category is found
+ create Category
+ Command -> Category
+ activate Category
+ Category -[hidden]> Command
+ deactivate Category
+
+ Command-[hidden]->Category
+ activate Category
+ Category -> Category :equals(newExpenseCategoryInString)
+ opt if equals check is true
+ Category -[hidden]> Category
+ Category --> Command
+ end
+ deactivate Category
+end
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/edit_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/edit_expense_command.puml
new file mode 100644
index 00000000000..b53ccb4a870
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/edit_expense_command.puml
@@ -0,0 +1,75 @@
+@startuml EditExpenseSequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+box Model MODEL_COLOR_T1
+participant "dataModel: Model" as Model order 3 MODEL_COLOR
+participant "expenseTracker: ExpenseTracker" as ExpenseTracker order 4 MODEL_COLOR
+participant "expenses: ExpenseList" as ExpenseList order 5 MODEL_COLOR
+end box
+
+box Logic LOGIC_COLOR_T1
+participant ":EditExpenseCommand" as Command LOGIC_COLOR
+participant ":CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+[->Command: execute(dataModel)
+activate Command
+Command -> Model: getFilteredExpenseList()
+activate Model
+Model --> Command: lastShownExpenseList
+deactivate Model
+Command -> Model: getFilteredCategoryList()
+activate Model
+Model --> Command: lastShownCategoryList
+deactivate Model
+
+Command -> Model
+ref over Model
+ Look for matching category in
+ lastShownCategoryList and
+ retrieve the category list
+end ref
+ref over Model
+ Obtain the expense from lastShownExpenseList
+ using the parsed index
+ Create new expense with the new values
+end ref
+Model --> Command
+
+Command -> Model: setExpense(target, editedExpense)
+activate Model
+Model -> ExpenseTracker: setExpense(target, editedExpense)
+activate ExpenseTracker
+ExpenseTracker -> ExpenseList: setExpense(target, editedExpense)
+activate ExpenseList
+ExpenseList -> ExpenseList: retrieve target's index from \nObservableList, \nset editedExpense at index
+ExpenseList --> ExpenseTracker
+deactivate ExpenseList
+ExpenseTracker -> ExpenseList: sortList()
+activate ExpenseList
+ExpenseList -> ExpenseList: sort expenses in \nObservableList\nby date
+ExpenseList --> ExpenseTracker
+deactivate ExpenseList
+ExpenseTracker --> Model
+deactivate ExpenseTracker
+Model -> Model: updateFilteredExpenseList\n(PREDICATE_SHOW_ALL_EXPENSES)
+activate Model
+deactivate Model
+Model --> Command
+deactivate Model
+create CommandResult
+Command -> CommandResult
+activate CommandResult
+CommandResult --> Command
+deactivate CommandResult
+' [<-- Command: result
+destroy Command
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/find_command.puml b/docs/diagrams/uml/sequence_diagrams/find_command.puml
new file mode 100644
index 00000000000..41de3a0a4e7
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/find_command.puml
@@ -0,0 +1,35 @@
+@startuml FindSequenceDiagram
+!include ../../style.puml
+
+box Logic LOGIC_COLOR_T1
+participant ":FindCommand" as FindCommand LOGIC_COLOR
+participant ":CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+box Model MODEL_COLOR_T1
+participant "model:Model" as Model order 3 MODEL_COLOR
+end box
+
+[-> FindCommand : execute(model)
+activate FindCommand
+FindCommand -> Model : updateFilteredPersonList(predicate)
+activate Model
+deactivate Model
+create CommandResult
+FindCommand -> CommandResult
+
+activate CommandResult
+CommandResult --> FindCommand
+deactivate CommandResult
+' FindCommand --> [ : result
+[<--FindCommand : result
+deactivate FindCommand
+destroy FindCommand
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/list_category_command.puml b/docs/diagrams/uml/sequence_diagrams/list_category_command.puml
new file mode 100644
index 00000000000..9e7fecfb3f9
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/list_category_command.puml
@@ -0,0 +1,44 @@
+@startuml ListCategorySequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+
+
+box Logic LOGIC_COLOR_T1
+participant ":ListCategoryCommand" as ListCategoryCommand LOGIC_COLOR
+participant ":CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+box Model MODEL_COLOR_T1
+participant "model:Model" as Model order 3 MODEL_COLOR
+end box
+
+[-> ListCategoryCommand : execute(model)
+activate ListCategoryCommand
+ListCategoryCommand -> Model : updateFilteredExpenseList(predicate)
+activate Model
+deactivate Model
+create CommandResult
+ListCategoryCommand -> CommandResult
+
+activate CommandResult
+CommandResult --> ListCategoryCommand
+deactivate CommandResult
+[<--ListCategoryCommand : result
+deactivate ListCategoryCommand
+destroy ListCategoryCommand
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/list_expenses_command.puml b/docs/diagrams/uml/sequence_diagrams/list_expenses_command.puml
new file mode 100644
index 00000000000..8d0b789ef05
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/list_expenses_command.puml
@@ -0,0 +1,83 @@
+@startuml ListExpensesSequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+
+box Model MODEL_COLOR_T1
+participant "model:Model" as Model order 4 MODEL_COLOR
+participant "filteredExpensesin\nExpenseTracker" as filterModel order 3 MODEL_COLOR
+end box
+
+box Logic LOGIC_COLOR_T1
+participant ":ListExpenseCommand" as ListExpenseCommand LOGIC_COLOR
+participant ":CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+[-> ListExpenseCommand : execute(model)
+activate ListExpenseCommand
+alt category predicate \nis present
+ ListExpenseCommand -> filterModel : filter.getCategory()
+ activate filterModel
+ filterModel --> ListExpenseCommand : category
+ deactivate filterModel
+ ListExpenseCommand -> Model: updateCategoryFilter(category)
+ activate Model
+ deactivate Model
+else else
+ ListExpenseCommand -> Model: updateCategoryFilter(null)
+ activate Model
+ deactivate Model
+end
+
+alt timespan predicate \nis present
+ ListExpenseCommand -> filterModel : filter.getTimespan()
+ activate filterModel
+ filterModel -> ListExpenseCommand : timespan
+ deactivate filterModel
+ ListExpenseCommand -> Model: updateTimespanFilter(timespan)
+ activate Model
+ deactivate Model
+else else
+ ListExpenseCommand -> Model: updateTimespanFilter(timespan)
+ activate Model
+ deactivate Model
+end
+
+opt category predicate \nis present
+ ListExpenseCommand -> ListExpenseCommand : combinedPredicate.and(categoryModel.get())
+ ListExpenseCommand -[hidden]-> Model
+end
+
+opt timespan \nis present
+ ListExpenseCommand -> ListExpenseCommand : combinedPredicate.and(timespanPredicate.get())
+ ListExpenseCommand -[hidden]-> Model
+end
+
+
+ListExpenseCommand -> Model : updateFilteredExpenseList(combinedPredicate)
+activate Model
+deactivate Model
+create CommandResult
+ListExpenseCommand -> CommandResult
+
+activate CommandResult
+CommandResult --> ListExpenseCommand
+deactivate CommandResult
+[<--ListExpenseCommand : result
+deactivate ListExpenseCommand
+destroy ListExpenseCommand
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+
+@enduml
diff --git a/docs/diagrams/uml/sequence_diagrams/list_recurring_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/list_recurring_expense_command.puml
new file mode 100644
index 00000000000..40753f245f9
--- /dev/null
+++ b/docs/diagrams/uml/sequence_diagrams/list_recurring_expense_command.puml
@@ -0,0 +1,42 @@
+@startuml ListRecurringExpensesSequenceDiagram
+!include ../../style.puml
+
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+box Logic LOGIC_COLOR_T1
+participant ":ListRecurringExpenseCommand" as ListRecurringExpenseCommand LOGIC_COLOR
+participant ":CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+box Model MODEL_COLOR_T1
+participant "model:Model" as Model order 3 MODEL_COLOR
+end box
+
+[-> ListRecurringExpenseCommand : execute(model)
+activate ListRecurringExpenseCommand
+ListRecurringExpenseCommand -> Model : updateFilteredExpenseList(predicate)
+activate Model
+deactivate Model
+create CommandResult
+ListRecurringExpenseCommand -> CommandResult
+
+activate CommandResult
+CommandResult --> ListRecurringExpenseCommand
+deactivate CommandResult
+[<--ListRecurringExpenseCommand : result
+deactivate ListRecurringExpenseCommand
+destroy ListRecurringExpenseCommand
+hide footbox
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+
+@enduml
diff --git a/docs/diagrams/uml/storage.puml b/docs/diagrams/uml/storage.puml
new file mode 100644
index 00000000000..bdda251db16
--- /dev/null
+++ b/docs/diagrams/uml/storage.puml
@@ -0,0 +1,109 @@
+@startuml storage
+
+interface UserPrefsStorage {
+ getUserPrefsFilePath(): Path
+ readUserPrefs(): Optional
+ saveUserPrefs(userPrefs: ReadOnlyUserPrefs): void
+}
+
+interface ExpenseTrackerStorage {
+ getExpenseTrackerFilePath(): Path
+ readExpenseTracker(): Optional
+ readExpenseTracker(filePath: Path): Optional
+ saveExpenseTracker(expenseTracker: ReadOnlyExpenseTracker): void
+ saveExpenseTracker(expenseTracker: ReadOnlyExpenseTracker, filePath: Path): void
+}
+
+interface Storage extends ExpenseTrackerStorage, UserPrefsStorage {
+}
+
+class JsonAdaptedBudget {
+ + JsonAdaptedBudget(source: Budget)
+ + JsonAdaptedBudget(amount: String)
+ + toModelType(): Budget
+ {static} MISSING_FIELD_MESSAGE_FORMAT: String
+ - amount: String
+}
+
+class JsonAdaptedCategory {
+ + JsonAdaptedCategory(source: Category)
+ + JsonAdaptedCategory(categoryName: String, summary: String)
+ + toModelType(): Category
+ {static} MISSING_FIELD_MESSAGE_FORMAT: String
+ - categoryName: String
+ - summary: String
+}
+
+class JsonAdaptedExpense {
+ + JsonAdaptedExpense(source: Expense)
+ + JsonAdaptedExpense(name: String, amount: String, date: String, category: String, category: JsonAdaptedCategory)
+ + toModelType(): Expense
+ {static} MISSING_FIELD_MESSAGE_FORMAT: String
+ - name: String
+ - amount: String
+ - date: String
+ - category: JsonAdaptedCategory
+}
+
+class JsonAdaptedRecurringExpenseManager {
+ + JsonAdaptedRecurringExpenseManager(source: RecurringExpenseManager)
+ + JsonAdaptedRecurringExpenseManager(expenseName: String, expenseAmount: String, expenseCategory: JsonAdaptedCategory, nextExpenseDate: String, startDate: String, endDate: String, recurringExpenseType: String)
+ + toModelType(): RecurringExpenseManager
+ {static} MISSING_FIELD_MESSAGE_FORMAT: String
+ - expenseName: String
+ - expenseAmount: String
+ - expenseCategory: JsonAdaptedCategory
+ - nextExpenseDate: String
+ - startDate: String
+ - endDate: String
+ - recurringExpenseType: String
+}
+
+class JsonExpenseTrackerStorage {
+ + JsonExpenseTrackerStorage(filePath: Path)
+ + getExpenseTrackerFilePath(): Path
+ + readExpenseTracker(filePath: Path): Optional
+ + saveExpenseTracker(expenseTracker: ReadOnlyExpenseTracker, filePath: Path): void
+ - {static} logger: Logger
+ - filePath: Path
+}
+
+class JsonSerializableExpenseTracker {
+ + JsonSerializableExpenseTracker(source: ReadOnlyExpenseTracker)
+ + JsonSerializableExpenseTracker(listOfCategories: List, listOfExpenses: List, budget: JsonAdaptedBudget, recurringGenerators: List)
+ + toModelType(): ExpenseTracker
+ - getAssociatedCategory(expense: Expense, expenseTracker: ExpenseTracker): Category
+ - getAssociatedCategoryForRecurring(recur: RecurringExpenseManager, expenseTracker: ExpenseTracker): Category
+ - categories: ArrayList
+ - expenses: ArrayList
+ - budget: JsonAdaptedBudget
+ - recurringGenerators: ArrayList
+}
+
+class JsonUserPrefsStorage {
+ + JsonUserPrefsStorage(filePath: Path)
+ + readUserPrefs(prefsFilePath: Path): Optional
+ - filePath: Path
+}
+
+class StorageManager {
+ + StorageManager(expenseTracker: ExpenseTrackerStorage, UserPrefsStorage: UserPrefsStorage)
+ - {static} logger: Logger
+ - expenseTrackerStorage: ExpenseTrackerStorage
+ - userPrefsStorage: UserPrefsStorage
+}
+
+
+Storage <|-- StorageManager
+ExpenseTrackerStorage <|-- JsonExpenseTrackerStorage
+UserPrefsStorage <|-- JsonUserPrefsStorage
+JsonExpenseTrackerStorage *-- "1" JsonSerializableExpenseTracker
+JsonSerializableExpenseTracker *-- "1" JsonAdaptedExpense
+JsonSerializableExpenseTracker *-- "1" JsonAdaptedCategory
+JsonSerializableExpenseTracker *-- "1" JsonAdaptedBudget
+JsonSerializableExpenseTracker *-- "1" JsonAdaptedRecurringExpenseManager
+' JsonAdaptedExpense *-- "1" Expense
+' JsonAdaptedCategory *-- "1" Category
+' JsonAdaptedBudget *-- "1" Budget
+' JsonAdaptedRecurringExpenseManager *-- "1" RecurringExpenseManager
+@enduml
diff --git a/docs/diagrams/uml/ui.puml b/docs/diagrams/uml/ui.puml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png
index 86c60246ccb..74bdb501671 100644
Binary files a/docs/images/ArchitectureDiagram.png and b/docs/images/ArchitectureDiagram.png differ
diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png
index 2f1346869d0..c1ed26b45ea 100644
Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ
diff --git a/docs/images/ComponentManagers.png b/docs/images/ComponentManagers.png
index b5764ff9273..b345322362b 100644
Binary files a/docs/images/ComponentManagers.png and b/docs/images/ComponentManagers.png differ
diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png
index fa327b39618..aedc85399b3 100644
Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ
diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png
index 9e9ba9f79e5..1371203e3bd 100644
Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ
diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png
index 04070af60d8..b40862a292a 100644
Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ
diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png
index e7b4c8880cd..17b35e322f5 100644
Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ
diff --git a/docs/images/RecurringExpenseStartUpSequenceDiagram.png b/docs/images/RecurringExpenseStartUpSequenceDiagram.png
new file mode 100644
index 00000000000..570d8baf4ec
Binary files /dev/null and b/docs/images/RecurringExpenseStartUpSequenceDiagram.png differ
diff --git a/docs/images/SetBudgetSequenceDiagram.png b/docs/images/SetBudgetSequenceDiagram.png
new file mode 100644
index 00000000000..7d30a7a1ad4
Binary files /dev/null and b/docs/images/SetBudgetSequenceDiagram.png differ
diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png
index 2533a5c1af0..de673b8ae5e 100644
Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ
diff --git a/docs/images/StorageUmlDiagram.png b/docs/images/StorageUmlDiagram.png
new file mode 100644
index 00000000000..fb531a8ea37
Binary files /dev/null and b/docs/images/StorageUmlDiagram.png differ
diff --git a/docs/images/Ui.png b/docs/images/Ui.png
index 5bd77847aa2..172be4f38d3 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 785e04dbab4..52527f26cd4 100644
Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ
diff --git a/docs/images/UiStartup.PNG b/docs/images/UiStartup.PNG
new file mode 100644
index 00000000000..9baaa67eb9d
Binary files /dev/null and b/docs/images/UiStartup.PNG differ
diff --git a/docs/images/Ui_Revised.png b/docs/images/Ui_Revised.png
new file mode 100644
index 00000000000..2a03fa4db10
Binary files /dev/null and b/docs/images/Ui_Revised.png differ
diff --git a/docs/images/activity_diagrams/addRecurringExpenseActivityDiagram.png b/docs/images/activity_diagrams/addRecurringExpenseActivityDiagram.png
new file mode 100644
index 00000000000..b3ccdb00438
Binary files /dev/null and b/docs/images/activity_diagrams/addRecurringExpenseActivityDiagram.png differ
diff --git a/docs/images/activity_diagrams/deleteRecurringExpenseActivityDiagram.png b/docs/images/activity_diagrams/deleteRecurringExpenseActivityDiagram.png
new file mode 100644
index 00000000000..b66c5b1537b
Binary files /dev/null and b/docs/images/activity_diagrams/deleteRecurringExpenseActivityDiagram.png differ
diff --git a/docs/images/activity_diagrams/editRecurringExpenseActivityDiagram.png b/docs/images/activity_diagrams/editRecurringExpenseActivityDiagram.png
new file mode 100644
index 00000000000..8265b1262b4
Binary files /dev/null and b/docs/images/activity_diagrams/editRecurringExpenseActivityDiagram.png differ
diff --git a/docs/images/demo/category/addcat.png b/docs/images/demo/category/addcat.png
new file mode 100644
index 00000000000..c9562726e8c
Binary files /dev/null and b/docs/images/demo/category/addcat.png differ
diff --git a/docs/images/demo/category/delcat.png b/docs/images/demo/category/delcat.png
new file mode 100644
index 00000000000..adb309657fd
Binary files /dev/null and b/docs/images/demo/category/delcat.png differ
diff --git a/docs/images/demo/category/edcat1.png b/docs/images/demo/category/edcat1.png
new file mode 100644
index 00000000000..2d0e8dbb38a
Binary files /dev/null and b/docs/images/demo/category/edcat1.png differ
diff --git a/docs/images/demo/category/edcat2.png b/docs/images/demo/category/edcat2.png
new file mode 100644
index 00000000000..3b6eb883a7a
Binary files /dev/null and b/docs/images/demo/category/edcat2.png differ
diff --git a/docs/images/demo/category/lcat.png b/docs/images/demo/category/lcat.png
new file mode 100644
index 00000000000..43c997d6ce2
Binary files /dev/null and b/docs/images/demo/category/lcat.png differ
diff --git a/docs/images/demo/category/sumcat.png b/docs/images/demo/category/sumcat.png
new file mode 100644
index 00000000000..9a760d9df76
Binary files /dev/null and b/docs/images/demo/category/sumcat.png differ
diff --git a/docs/images/demo/expense/add1.png b/docs/images/demo/expense/add1.png
new file mode 100644
index 00000000000..d8e27a51b46
Binary files /dev/null and b/docs/images/demo/expense/add1.png differ
diff --git a/docs/images/demo/expense/add2.png b/docs/images/demo/expense/add2.png
new file mode 100644
index 00000000000..cf95388b1f2
Binary files /dev/null and b/docs/images/demo/expense/add2.png differ
diff --git a/docs/images/demo/expense/delete.png b/docs/images/demo/expense/delete.png
new file mode 100644
index 00000000000..895539c676c
Binary files /dev/null and b/docs/images/demo/expense/delete.png differ
diff --git a/docs/images/demo/expense/edexp1.png b/docs/images/demo/expense/edexp1.png
new file mode 100644
index 00000000000..dfd8de84fa8
Binary files /dev/null and b/docs/images/demo/expense/edexp1.png differ
diff --git a/docs/images/demo/expense/edexp2.png b/docs/images/demo/expense/edexp2.png
new file mode 100644
index 00000000000..a73bc45f712
Binary files /dev/null and b/docs/images/demo/expense/edexp2.png differ
diff --git a/docs/images/demo/expense/find.png b/docs/images/demo/expense/find.png
new file mode 100644
index 00000000000..b3afb64e7ac
Binary files /dev/null and b/docs/images/demo/expense/find.png differ
diff --git a/docs/images/demo/expense/list1.png b/docs/images/demo/expense/list1.png
new file mode 100644
index 00000000000..e663ab2e417
Binary files /dev/null and b/docs/images/demo/expense/list1.png differ
diff --git a/docs/images/demo/expense/list2.png b/docs/images/demo/expense/list2.png
new file mode 100644
index 00000000000..1d61355a614
Binary files /dev/null and b/docs/images/demo/expense/list2.png differ
diff --git a/docs/images/demo/expense/list3.png b/docs/images/demo/expense/list3.png
new file mode 100644
index 00000000000..1d974a55836
Binary files /dev/null and b/docs/images/demo/expense/list3.png differ
diff --git a/docs/images/demo/general/autocomplete_a1.png b/docs/images/demo/general/autocomplete_a1.png
new file mode 100644
index 00000000000..8146e388ea6
Binary files /dev/null and b/docs/images/demo/general/autocomplete_a1.png differ
diff --git a/docs/images/demo/general/autocomplete_a2.png b/docs/images/demo/general/autocomplete_a2.png
new file mode 100644
index 00000000000..18f21fd585f
Binary files /dev/null and b/docs/images/demo/general/autocomplete_a2.png differ
diff --git a/docs/images/demo/general/autocomplete_b1.png b/docs/images/demo/general/autocomplete_b1.png
new file mode 100644
index 00000000000..fc5613f2495
Binary files /dev/null and b/docs/images/demo/general/autocomplete_b1.png differ
diff --git a/docs/images/demo/general/autocomplete_b2.png b/docs/images/demo/general/autocomplete_b2.png
new file mode 100644
index 00000000000..7770f331a9f
Binary files /dev/null and b/docs/images/demo/general/autocomplete_b2.png differ
diff --git a/docs/images/demo/general/clear.png b/docs/images/demo/general/clear.png
new file mode 100644
index 00000000000..23ba9d2bd4e
Binary files /dev/null and b/docs/images/demo/general/clear.png differ
diff --git a/docs/images/demo/general/set.png b/docs/images/demo/general/set.png
new file mode 100644
index 00000000000..f60069750b5
Binary files /dev/null and b/docs/images/demo/general/set.png differ
diff --git a/docs/images/demo/general/summary.png b/docs/images/demo/general/summary.png
new file mode 100644
index 00000000000..d3d3cd6e27e
Binary files /dev/null and b/docs/images/demo/general/summary.png differ
diff --git a/docs/images/demo/intro/fasttrack_labeled_1.png b/docs/images/demo/intro/fasttrack_labeled_1.png
new file mode 100644
index 00000000000..bfe5d50ccbd
Binary files /dev/null and b/docs/images/demo/intro/fasttrack_labeled_1.png differ
diff --git a/docs/images/demo/intro/fasttrack_labeled_2.png b/docs/images/demo/intro/fasttrack_labeled_2.png
new file mode 100644
index 00000000000..703cd6b6506
Binary files /dev/null and b/docs/images/demo/intro/fasttrack_labeled_2.png differ
diff --git a/docs/images/demo/intro/fasttrack_labeled_3.png b/docs/images/demo/intro/fasttrack_labeled_3.png
new file mode 100644
index 00000000000..282410c126a
Binary files /dev/null and b/docs/images/demo/intro/fasttrack_labeled_3.png differ
diff --git a/docs/images/demo/recurring_expense/addrec1.png b/docs/images/demo/recurring_expense/addrec1.png
new file mode 100644
index 00000000000..32adbbf66e3
Binary files /dev/null and b/docs/images/demo/recurring_expense/addrec1.png differ
diff --git a/docs/images/demo/recurring_expense/addrec2.png b/docs/images/demo/recurring_expense/addrec2.png
new file mode 100644
index 00000000000..045aee9d392
Binary files /dev/null and b/docs/images/demo/recurring_expense/addrec2.png differ
diff --git a/docs/images/demo/recurring_expense/delrec.png b/docs/images/demo/recurring_expense/delrec.png
new file mode 100644
index 00000000000..c4b6c997326
Binary files /dev/null and b/docs/images/demo/recurring_expense/delrec.png differ
diff --git a/docs/images/demo/recurring_expense/edrec1.png b/docs/images/demo/recurring_expense/edrec1.png
new file mode 100644
index 00000000000..8f6a8dd21cc
Binary files /dev/null and b/docs/images/demo/recurring_expense/edrec1.png differ
diff --git a/docs/images/demo/recurring_expense/edrec2.png b/docs/images/demo/recurring_expense/edrec2.png
new file mode 100644
index 00000000000..7235cceb755
Binary files /dev/null and b/docs/images/demo/recurring_expense/edrec2.png differ
diff --git a/docs/images/demo/recurring_expense/lrec.png b/docs/images/demo/recurring_expense/lrec.png
new file mode 100644
index 00000000000..89df5736cc2
Binary files /dev/null and b/docs/images/demo/recurring_expense/lrec.png differ
diff --git a/docs/images/fasttrack_logo.png b/docs/images/fasttrack_logo.png
new file mode 100644
index 00000000000..5b1d7175f8d
Binary files /dev/null and b/docs/images/fasttrack_logo.png differ
diff --git a/docs/images/gitsac.png b/docs/images/gitsac.png
new file mode 100644
index 00000000000..aabeaaf154c
Binary files /dev/null and b/docs/images/gitsac.png differ
diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png
index b1f70470137..98fa6bb1064 100644
Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ
diff --git a/docs/images/jinbesan.png b/docs/images/jinbesan.png
new file mode 100644
index 00000000000..218248ea00a
Binary files /dev/null and b/docs/images/jinbesan.png differ
diff --git a/docs/images/nicleejy.png b/docs/images/nicleejy.png
new file mode 100644
index 00000000000..c910c7c1f64
Binary files /dev/null and b/docs/images/nicleejy.png differ
diff --git a/docs/images/randallnhr.png b/docs/images/randallnhr.png
new file mode 100644
index 00000000000..c0a7802e2aa
Binary files /dev/null and b/docs/images/randallnhr.png differ
diff --git a/docs/images/sequence_diagrams/AddExpenseSequenceDiagram.png b/docs/images/sequence_diagrams/AddExpenseSequenceDiagram.png
new file mode 100644
index 00000000000..90300281ae5
Binary files /dev/null and b/docs/images/sequence_diagrams/AddExpenseSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/DeleteExpenseSequenceDiagram.png b/docs/images/sequence_diagrams/DeleteExpenseSequenceDiagram.png
new file mode 100644
index 00000000000..5dd1b61c4fb
Binary files /dev/null and b/docs/images/sequence_diagrams/DeleteExpenseSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/EditExpenseFindPartSequenceDiagram.png b/docs/images/sequence_diagrams/EditExpenseFindPartSequenceDiagram.png
new file mode 100644
index 00000000000..68106e3c8cb
Binary files /dev/null and b/docs/images/sequence_diagrams/EditExpenseFindPartSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/EditExpenseSequenceDiagram.png b/docs/images/sequence_diagrams/EditExpenseSequenceDiagram.png
new file mode 100644
index 00000000000..25e22824399
Binary files /dev/null and b/docs/images/sequence_diagrams/EditExpenseSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/FindSequenceDiagram.png b/docs/images/sequence_diagrams/FindSequenceDiagram.png
new file mode 100644
index 00000000000..f6645bf079d
Binary files /dev/null and b/docs/images/sequence_diagrams/FindSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/ListCategorySequenceDiagram.png b/docs/images/sequence_diagrams/ListCategorySequenceDiagram.png
new file mode 100644
index 00000000000..92c3622873f
Binary files /dev/null and b/docs/images/sequence_diagrams/ListCategorySequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/ListExpensesSequenceDiagram.png b/docs/images/sequence_diagrams/ListExpensesSequenceDiagram.png
new file mode 100644
index 00000000000..48bba05c6e8
Binary files /dev/null and b/docs/images/sequence_diagrams/ListExpensesSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/ListRecurringExpensesSequenceDiagram.png b/docs/images/sequence_diagrams/ListRecurringExpensesSequenceDiagram.png
new file mode 100644
index 00000000000..514eb00b6b7
Binary files /dev/null and b/docs/images/sequence_diagrams/ListRecurringExpensesSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.png b/docs/images/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.png
new file mode 100644
index 00000000000..85888d90559
Binary files /dev/null and b/docs/images/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.png differ
diff --git a/docs/images/sequence_diagrams/SetBudgetSequenceDiagram.png b/docs/images/sequence_diagrams/SetBudgetSequenceDiagram.png
new file mode 100644
index 00000000000..91685941870
Binary files /dev/null and b/docs/images/sequence_diagrams/SetBudgetSequenceDiagram.png differ
diff --git a/docs/images/shirsho-12.png b/docs/images/shirsho-12.png
new file mode 100644
index 00000000000..d0c0f4b567f
Binary files /dev/null and b/docs/images/shirsho-12.png differ
diff --git a/docs/images/uml_diagrams/commands/add_commands.png b/docs/images/uml_diagrams/commands/add_commands.png
new file mode 100644
index 00000000000..4ada1a3f94e
Binary files /dev/null and b/docs/images/uml_diagrams/commands/add_commands.png differ
diff --git a/docs/images/uml_diagrams/commands/commands.png b/docs/images/uml_diagrams/commands/commands.png
new file mode 100644
index 00000000000..e4c48d96789
Binary files /dev/null and b/docs/images/uml_diagrams/commands/commands.png differ
diff --git a/docs/images/uml_diagrams/commands/delete_commands.png b/docs/images/uml_diagrams/commands/delete_commands.png
new file mode 100644
index 00000000000..ac28a747d37
Binary files /dev/null and b/docs/images/uml_diagrams/commands/delete_commands.png differ
diff --git a/docs/images/uml_diagrams/commands/edit_commands.png b/docs/images/uml_diagrams/commands/edit_commands.png
new file mode 100644
index 00000000000..0b91081d0dc
Binary files /dev/null and b/docs/images/uml_diagrams/commands/edit_commands.png differ
diff --git a/docs/images/uml_diagrams/commands/general_commands.png b/docs/images/uml_diagrams/commands/general_commands.png
new file mode 100644
index 00000000000..87d7c356f39
Binary files /dev/null and b/docs/images/uml_diagrams/commands/general_commands.png differ
diff --git a/docs/images/uml_diagrams/commands/list_commands.png b/docs/images/uml_diagrams/commands/list_commands.png
new file mode 100644
index 00000000000..5a8b5dd30b1
Binary files /dev/null and b/docs/images/uml_diagrams/commands/list_commands.png differ
diff --git a/docs/index.md b/docs/index.md
index 7601dbaad0d..c29112653a4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,17 +1,17 @@
---
layout: page
-title: AddressBook Level-3
+title: FastTrack
---
-[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions)
-[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3)
+[![CI Status](https://github.com/AY2223S2-CS2103T-W09-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S2-CS2103T-W09-2/tp/actions)
+[![codecov](https://codecov.io/gh/AY2223S2-CS2103T-W09-2/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2223S2-CS2103T-W09-2/tp/)
![Ui](images/Ui.png)
-**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface).
+**FastTrack is the answer to your expense management prayers.** It is a desktop application with a GUI. Most of the user interactions, however, happen using a CLI (Command Line Interface).
-* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start).
-* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start.
+* If you are interested in using FastTrack, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start).
+* If you are interested about developing FastTrack, the [**Developer Guide**](DeveloperGuide.html) is a good place to start.
**Acknowledgements**
diff --git a/docs/team/gitsac.md b/docs/team/gitsac.md
new file mode 100644
index 00000000000..c00800de029
--- /dev/null
+++ b/docs/team/gitsac.md
@@ -0,0 +1,57 @@
+---
+layout: page
+title: Isaac's Project Portfolio Page
+---
+
+### Overview
+
+FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user.
+
+### Summary of Contributions
+
+- Code contributed: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=gitsac&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other)
+
+Enhancements implemented:
+- Helped with implementation of `Category` class.
+
+ - Defined the `Category` class with its fields. (Pull request [#30](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/30))
+ - Edited storage system to accommodate `Category` class. (Pull request [#29](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/29))
+
+
+- Helped with the implementation of `RecurringExpenseManager` class.
+
+ - Edited storage system to accommodate `Category` class. (Pull request [#95](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/95))
+
+
+- Implemented `edit` function for all 3 main classes used (`Category`, `Expense` and `RecurringExpenseManager`)
+
+ - Implemented `EditExpenseCommand` along with its necessary helper parser class. (Pull request [#77](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/79))
+ - Implemented `EditCategoryCommand` along with necessary parser class. (Pull request [#78](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/78))
+ - Implemented `EditRecurringExpenseManagerCommand` along with necessary parser class. (Pull request [#130](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/130))
+
+
+- Added basic startup data that was adapted for FastTrack's usage. (Morphed from AB3's given sample data) (Pull request [#107](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/107))
+
+
+- Added functionality that causes list of `Expense` in FastTrack to be sorted by date upon any operations (adding/deleting expenses) (Pull request [#136](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/136))
+
+
+Contributions to the UG:
+- Wrote about features in initial draft and added tables denoting the parameters used as well as simple explanations.
+
+Contributions to the DG:
+- Worked on the Implementations portion of the DG.
+- Wrote about the Effort section of the DG.
+- Sketched multiple sequence diagrams that were translated through PlantUML to be used in the DG.
+
+Contributions to team-based tasks:
+- Participated in weekly (sometimes biweekly) meetings to discuss project structure and direction.
+- Took part actively in debugging other teammate's issues.
+- Fixed several bugs reported from PE Dry Run ([#167](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/167), [#170](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/170), [#179](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/179)).
+- Helped to regulate pull requests from team-mates and merged them only when they passed CI and internal test cases.
+
+Review/Mentoring Contributions:
+- Reviewed multiple PRs made by teammates ([#101](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/101), [#90](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/90), [#72](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/72)).
+
+Contributions beyond team project:
+- Reported bugs for another team during the PE-Dry run (T11-4): HospiSearch.
diff --git a/docs/team/jinbesan.md b/docs/team/jinbesan.md
new file mode 100644
index 00000000000..1f9e92e1aba
--- /dev/null
+++ b/docs/team/jinbesan.md
@@ -0,0 +1,50 @@
+---
+layout: page
+title: Wen Hong's Project Portfolio Page
+---
+
+### Overview
+
+FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user.
+
+### Summary of Contributions
+
+- Code contributed: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=jinbesan&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other)
+
+Enhancements implemented:
+
+- Implementation of `find` Command (Pull request [#90](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/90))
+ - Addition of `ExpenseContainsKeywordsPredicate`
+- Implementation of `list` Command
+ - Addition of `ExpenseInCategoryPredicate` (Pull request [#101](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/101))
+ - Addition of `ExpenseInTimespanPredicate` (Pull request [#104](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/104))
+- Implementation of `lrec` Command (Pull request [#212](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/212))
+
+- Updated `help` window (Pull request [#66](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/66))
+- Added TestUtils (Pull request [#86](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/86))
+
+- Helped with implementation of `ParserUtil` class.
+
+Contributions to the UG:
+- Wrote about features in initial draft and added tables denoting the parameters used as well as simple explanations. (Pull request [#126](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/126))
+- Wrote Introduction, Why to use FastTrack, Purpose of Guide, Understanding Guide, Quick Start, GUI Walkthrough sections of the User Guide (Pull request [#212](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/212))
+- Rearranged structure of User Guide to be more user-friendly (Pull request [#236](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/236))
+- Reformatting of tables for commands and tips
+
+Contributions to the DG:
+- Wrote about features in initial draft and added tables denoting the parameters used as well as some use cases. (Pull request [#105](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/105), Pull request [#127](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/127))
+- Sketched multiple sequence diagrams that were translated through PlantUML to be used in the DG.
+
+Contributions to team-based tasks:
+- Participated in weekly (sometimes biweekly) meetings to discuss project structure and direction.
+- Proposed addition of Recurring Expenses feature to create selling point of app
+- Participated in discussion of project architecture
+- Suggested structure of implementation of Recurring Expenses.
+- Took part actively in debugging other teammate's issues.
+- Helped to regulate pull requests from team-mates and merged them only when they passed CI and internal test cases.
+
+Review/Mentoring Contributions:
+- Reviewed multiple PRs made by teammates ([#146](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/146), [#89](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/89), [#145](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/145)).
+
+Contributions beyond team project:
+- Reported bugs for another team during the PE-Dry run (T15-3): Vimification.
diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md
deleted file mode 100644
index 773a07794e2..00000000000
--- a/docs/team/johndoe.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-layout: page
-title: John Doe's Project Portfolio Page
----
-
-### Project: AddressBook Level 3
-
-AddressBook - Level 3 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.
-
-Given below are my contributions to the project.
-
-* **New Feature**: 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}*
-
-* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys.
-
-* **Code contributed**: [RepoSense link]()
-
-* **Project management**:
- * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub
-
-* **Enhancements to existing features**:
- * Updated the GUI color scheme (Pull requests [\#33](), [\#34]())
- * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]())
-
-* **Documentation**:
- * User Guide:
- * Added documentation for the features `delete` and `find` [\#72]()
- * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]()
- * Developer Guide:
- * Added implementation details of the `delete` feature.
-
-* **Community**:
- * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]()
- * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]())
- * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]())
- * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]())
-
-* **Tools**:
- * Integrated a third party library (Natty) to the project ([\#42]())
- * Integrated a new Github plugin (CircleCI) to the team repo
-
-* _{you can add/remove categories in the list above}_
diff --git a/docs/team/nicleejy.md b/docs/team/nicleejy.md
new file mode 100644
index 00000000000..aa0b68db4fd
--- /dev/null
+++ b/docs/team/nicleejy.md
@@ -0,0 +1,47 @@
+---
+layout: page
+title: Nicholas's Project Portfolio Page
+---
+### Overview
+
+FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user.
+
+### Summary of Contributions
+
+- Code contributed: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=nicleejy&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other)
+
+
+Enhancements implemented:
+- Implemented new category autocomplete feature which allows users to autocomplete category names from a list of suggestions using arrow/enter/tab keys (Pull request [#148](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/148))
+- Added new UI screen for recurring expense feature (Pull request [#145](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/145))
+- Implemented the expense summary statistics feature (Pull request [#111](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/111))
+ - Added new UI section for summary statistics data
+ - Implemented new `AnalyticModelManager` class to manage expense data state
+ - Utilised Observer Pattern to integrate expense statistics data into `StatisticsPanel` UI component, ensuring statistics are updated in real time
+- Implemented add expense feature (Pull request [#72](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/72))
+ - Implemented `AddExpenseCommand` which encapsulates the command request details
+ - Added `ExpenseCommandParser` containing various parser methods to parse dates and prices to interpret the command
+- Added sample data for recurring expenses (Morphed from AB3's given sample data) (Pull request [#224](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/224))
+
+Contributions to the UG:
+- Wrote introduction section for the initial draft of the UG (Pull request [#124](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/124))
+- Updated second draft of UG (Pull request [#213](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/213))
+ - Update all feature sections
+ - added FAQ section
+ - Added annotated diagrams for each command and GUI walkthrough
+
+Contributions to the DG:
+- Add expense summary feature and implementation details to DG (Pull request [#110](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/110))
+- Sketched and implemented Activity Diagrams in PlantUML for recurring expense (Pull request [#218](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/218))
+- Add writeup for autocompletion feature in the DG (Pull request [#218](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/218))
+
+Contributions to team-based tasks:
+- Participated in weekly (sometimes biweekly) meetings to discuss project structure and direction.
+- Took part actively in debugging other teammate's issues.
+- Fixed [several bugs](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues?q=is%3Aissue+is%3Aclosed+assignee%3Anicleejy+pe-d) reported from PE Dry Run
+
+Review/Mentoring Contributions:
+- Reviewed [23 PRs](https://github.com/AY2223S2-CS2103T-W09-2/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3A%40me) made by teammates
+
+Contributions beyond team project:
+- Reported bugs for another team during the PE-Dry run (F10-1): OfficeConnect.
diff --git a/docs/team/randallnhr.md b/docs/team/randallnhr.md
new file mode 100644
index 00000000000..febdb74e0da
--- /dev/null
+++ b/docs/team/randallnhr.md
@@ -0,0 +1,55 @@
+---
+layout: page
+title: Randall's Project Portfolio Page
+---
+
+### Overview
+
+FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user.
+
+### Summary of Contributions
+
+**Code contributed:**
+
+The following [link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=randallnhr&breakdown=true&sort=groupTitle+dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs%7Efunctional-code%7Etest-code%7Eother) is my code contribution.
+
+#### **Enhancements implemented:**
+* Implemented commands for `Category`
+ * `addcat` - allows users of FastTrack to add a new `Category` into FastTrack. (PR [#68](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/68))
+ * Allow users to add category without summary (PR [#118](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/118))
+ * `delcat` - allows users to delete an existing `Category` in FastTrack. (PR [#68](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/68))
+ * Expenses with the deleted category will have its category replaced with the `MiscellaneuosCategory`. (PR [#109](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/109))
+ * `lcat` - allows users to list all added `Category`, used to determine index for edit and delete category commands. (PR [#68](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/68))
+ * `sumcat` - allows users to view category summary. (PR [#119](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/119))
+* Implemented `CLEAR` command. (PR [#120](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/120))
+ * What it does: Wipes the storage of FastTrack to a clean slate. This is useful when the user first opens FastTrack and wants to delete the sample data.
+* Implemented `Budget` class and linked it to the UI to update statistics. (PR [#138](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/138))
+ * What it does: Allows users to add a monthly budget into FastTrack. This is used in conjunction with the Statistics feature to allow users to have an easy way to see how much of the budget has been utilised.
+* Implemented commands for `RecurringExpenseManager`
+ * `addrec` - allows users to add `RecurringExpenseManager` objects into FastTrack. (PR [#140](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/140))
+ * `delrec` - allows users to delete a `RecurringExpenseManager` object. (PR [#140](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/140))
+
+#### **Contributions to the UG:**
+* Added command summary for:
+ * Expense commands
+ * Category commands
+ * General commands
+
+#### **Contributions to the DG:**
+* Added several use cases. (PR [#37](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/37))
+* Added purpose of the guide, how to use this guide and acknowledgement. (PR [#209](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/209))
+* Added Recurring Expense implementation. (PR [#222](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/222))
+* Added Budget implementation and linked to Statistics implementation. (PR [#222](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/222))
+* Added writeup for category features: (PR [#222](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/222))
+ * Adding a category
+ * Deleting a category
+* Created sequence diagrams for `set` and Recurring Expense feature. (PR [#235](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/235))
+* Created PlantUML diagrams for high level architecture and class diagrams. (PR [#249](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/249))
+#### **Contributions to team-based tasks:**
+* Organised weekly meetings to discuss project structure and direction.
+* Took part actively in debugging other teammate's issues.
+
+#### **Review/Mentoring Contributions:**
+* Reviewed several [PRs](https://github.com/AY2223S2-CS2103T-W09-2/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3A%40me) made by teammates.
+
+#### **Contributions beyond team project:**
diff --git a/docs/team/shirsho-12.md b/docs/team/shirsho-12.md
new file mode 100644
index 00000000000..180ef3726f7
--- /dev/null
+++ b/docs/team/shirsho-12.md
@@ -0,0 +1,72 @@
+---
+layout: page
+title: Shirshajit's Project Portfolio Page
+---
+
+### Project: FastTrack
+
+FastTrack is an expense tracking app that helps computing students keep track of their expenses by providing a simple and convenient command-line interface. It is optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, FastTrack can get your expense management tasks done faster than traditional GUI apps.
+
+Given below are my contributions to the project.
+
+### Summary of Contributions
+
+- **New Feature**: `Recurring Expenses`: Created the recurring expense functionality
+
+ - What it does: Creates recurring expenses for the user, i.e., expenses that occur at regular intervals. Theese expenses are automatically generated based on the user's input and current date. The user can also view all recurring expenses, delete recurring expenses, and mark recurring expenses as done.
+ - Justification: This feature improves the product significantly because a large number of users would prefer to track their recurring expenses. This feature also allows the user to save time by not having to manually create recurring expenses.
+ - Highlights: This enhancement affects existing commands and commands to be added in the future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands and the logic to handle the recurring expenses.
+
+- **New Feature**: `Category` and `Expense` models: Created the main Category and Expense models.
+
+ - What it does: The Category model is used to store the different categories of expenses that the user can add. The Expense model is used to store the different expenses that the user can add.
+ - Justification: This feature improves the product significantly because it allows the user to add expenses and categorize them. This feature also allows the user to save time by not having to manually create recurring expenses.
+ - Highlights: This enhancement affects existing commands and commands to be added in the future. It required an in-depth analysis of design alternatives. The implementation was challenging as it required changes to existing commands and the logic to handle the recurring expenses.
+
+- **Code Contributions**: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=shirsho-12&breakdown=true&sort=groupTitle&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other)
+
+- **Project Management**:
+ - Managed releases `v1.1` - `v1.4` (3 releases) on GitHub
+ - Managed issue creation and assignment, milestone creation, issue labelling, and issue closing
+ - Enforced coding standards and code quality
+ - Managed the project's [issue tracker](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues)
+ - Managed the project's [pull requests](https://github.com/AY2223S2-CS2103T-W09-2/tp/pulls)
+ - Managed the project [repository](https://github.com/AY2223S2-CS2103T-W09-2/tp) with the help of Isaac,[@gitsac](https://github.com/gitsac/), and Nicholas, [@niceleejy](https://github.com/niceleejy/).
+
+**Enhancements implemented**:
+
+- Implemented the recurring expense generation functionality
+- Developed Expense Type enums for the different frequency types of recurring expenses
+- Implemented functionality to strip out additional whitespace in user input
+
+**Contributions to the UG:**
+
+- Added documentation for the Category and Expense models
+- Added documentation for the recurring expense functionality
+
+**Contributions to the DG:**
+
+- Created Activity Diagrams for all the commands
+- Created UML Diagrams for the Category and Expense models, Commands, and the Parser
+- Designed the architecture diagrams for the project
+
+**Contributions to team-based tasks:**
+
+- Managed the setting up of the project repository and test suite
+- Created test cases for multiple commands, models, and storage
+- Reviewed and merged multiple pull requests
+- Redesigned the architecture of the Command classes to make it more extensible
+- Fixed a number of bugs in the parser and storage classes
+- Fixed test cases that were failing due to changes in the codebase
+- Refactored the codebase to improve code quality
+
+**Review/Mentoring Contributions:**
+
+- Reviewed and provided feedback on multiple pull requests
+- Reviewed and provided feedback on multiple issues
+- Reviewed and provided feedback on multiple code quality issues
+
+**Contributions beyond team project:**
+
+- Reported bugs and suggestions for improvement for other team projects
+- Participated in the discussion forum and helped other students with their queries
diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md
index 880c701042f..6c167fa06b3 100644
--- a/docs/tutorials/AddRemark.md
+++ b/docs/tutorials/AddRemark.md
@@ -16,16 +16,16 @@ We’ll assume that you have already set up the development environment as outli
Looking in the `logic.command` package, you will notice that each existing command have their own class. All the commands inherit from the abstract class `Command` which means that they must override `execute()`. Each `Command` returns an instance of `CommandResult` upon success and `CommandResult#feedbackToUser` is printed to the `ResultDisplay`.
-Let’s start by creating a new `RemarkCommand` class in the `src/main/java/seedu/address/logic/command` directory.
+Let’s start by creating a new `RemarkCommand` class in the `src/main/java/fasttrack/logic/command` directory.
For now, let’s keep `RemarkCommand` as simple as possible and print some output. We accomplish that by returning a `CommandResult` with an accompanying message.
**`RemarkCommand.java`:**
``` java
-package seedu.address.logic.commands;
+package fasttrack.logic.commands;
-import seedu.address.model.Model;
+import fasttrack.model.DataModelodel;
/**
* Changes the remark of an existing person in the address book.
@@ -35,7 +35,7 @@ public class RemarkCommand extends Command {
public static final String COMMAND_WORD = "remark";
@Override
- public CommandResult execute(Model model) {
+ public CommandResult execute(Model dataModel) {
return new CommandResult("Hello from remark");
}
}
@@ -77,7 +77,7 @@ Following the convention in other commands, we add relevant messages as constant
"Remark command not implemented yet";
@Override
- public CommandResult execute(Model model) throws CommandException {
+ public CommandResult execute(Model dataModel) throws CommandException {
throw new CommandException(MESSAGE_NOT_IMPLEMENTED_YET);
}
```
@@ -111,7 +111,7 @@ public class RemarkCommand extends Command {
this.remark = remark;
}
@Override
- public CommandResult execute(Model model) throws CommandException {
+ public CommandResult execute(Model dataModel) throws CommandException {
throw new CommandException(
String.format(MESSAGE_ARGUMENTS, index.getOneBased(), remark));
}
@@ -223,13 +223,13 @@ public RemarkCommand parse(String args) throws ParseException {
If you are stuck, check out the sample
[here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a).
-## Add `Remark` to the model
+## Add `Remark` to the dataModel
-Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person.
+Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` dataModel. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person.
### Add a new `Remark` class
-Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code.
+Create a new `Remark` in `seedu.address.dataModel.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code.
A copy-paste and search-replace later, you should have something like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-41bb13c581e280c686198251ad6cc337cd5e27032772f06ed9bf7f1440995ece). Note how `Remark` has no constrains and thus does not require input
validation.
@@ -295,7 +295,7 @@ While the changes to code may be minimal, the test data will have to be updated
-:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
+:exclamation: You must delete AddressBook’s storage file located at `/data/fastTrack.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
@@ -336,8 +336,8 @@ save it with `Model#setPerson()`.
public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s";
//...
@Override
- public CommandResult execute(Model model) throws CommandException {
- List lastShownList = model.getFilteredPersonList();
+ public CommandResult execute(Model dataModel) throws CommandException {
+ List lastShownList = dataModel.getFilteredPersonList();
if (index.getZeroBased() >= lastShownList.size()) {
throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
@@ -348,8 +348,8 @@ save it with `Model#setPerson()`.
personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(),
personToEdit.getAddress(), remark, personToEdit.getTags());
- model.setPerson(personToEdit, editedPerson);
- model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ dataModel.setPerson(personToEdit, editedPerson);
+ dataModel.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
return new CommandResult(generateSuccessMessage(editedPerson));
}
diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md
index f29169bc924..a59bf96ae0a 100644
--- a/docs/tutorials/RemovingFields.md
+++ b/docs/tutorials/RemovingFields.md
@@ -28,7 +28,7 @@ IntelliJ IDEA provides a refactoring tool that can identify *most* parts of a re
### Assisted refactoring
-The `address` field in `Person` is actually an instance of the `seedu.address.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu.
+The `address` field in `Person` is actually an instance of the `seedu.address.dataModel.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu.
* :bulb: To make things simpler, you can unselect the options `Search in comments and strings` and `Search for text occurrences`
![Usages detected](../images/remove/UnsafeDelete.png)
diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md
index 4fb62a83ef6..67079a257e5 100644
--- a/docs/tutorials/TracingCode.md
+++ b/docs/tutorials/TracingCode.md
@@ -122,12 +122,12 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
//Parse user input from String to a Command
Command command = addressBookParser.parseCommand(commandText);
//Executes the Command and stores the result
- commandResult = command.execute(model);
+ commandResult = command.execute(dataModel);
try {
- //We can deduce that the previous line of code modifies model in some way
+ //We can deduce that the previous line of code modifies dataModel in some way
// since it's being stored here.
- storage.saveAddressBook(model.getAddressBook());
+ storage.saveAddressBook(dataModel.getAddressBook());
} catch (IOException ioe) {
throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe);
}
@@ -187,26 +187,26 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
**`EditCommand#execute()`:**
``` java
@Override
- public CommandResult execute(Model model) throws CommandException {
+ public CommandResult execute(Model dataModel) throws CommandException {
...
Person personToEdit = lastShownList.get(index.getZeroBased());
Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor);
- if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) {
+ if (!personToEdit.isSamePerson(editedPerson) && dataModel.hasPerson(editedPerson)) {
throw new CommandException(MESSAGE_DUPLICATE_PERSON);
}
- model.setPerson(personToEdit, editedPerson);
- model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ dataModel.setPerson(personToEdit, editedPerson);
+ dataModel.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson));
}
```
-1. As suspected, `command#execute()` does indeed make changes to the `model` object. Specifically,
+1. As suspected, `command#execute()` does indeed make changes to the `dataModel` object. Specifically,
* it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the person data.
* it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked.
- * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component)
+ * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#dataModel-component)
1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
@@ -217,7 +217,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component.
- :bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
+
:bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(dataModel.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability):
diff --git a/fasttrack.log.0 b/fasttrack.log.0
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/fasttrack.log.0.1 b/fasttrack.log.0.1
new file mode 100644
index 00000000000..e0b22d7066e
Binary files /dev/null and b/fasttrack.log.0.1 differ
diff --git a/preferences.json b/preferences.json
new file mode 100644
index 00000000000..1d8ca70fd1d
--- /dev/null
+++ b/preferences.json
@@ -0,0 +1,11 @@
+{
+ "guiSettings" : {
+ "windowWidth" : 1000.0,
+ "windowHeight" : 700.0,
+ "windowCoordinates" : {
+ "x" : 208,
+ "y" : 25
+ }
+ },
+ "expenseTrackerFilePath" : "data/fastTrack.json"
+}
diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/fasttrack/AppParameters.java
similarity index 93%
rename from src/main/java/seedu/address/AppParameters.java
rename to src/main/java/fasttrack/AppParameters.java
index ab552c398f3..6398e6451cc 100644
--- a/src/main/java/seedu/address/AppParameters.java
+++ b/src/main/java/fasttrack/AppParameters.java
@@ -1,4 +1,4 @@
-package seedu.address;
+package fasttrack;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -6,9 +6,9 @@
import java.util.Objects;
import java.util.logging.Logger;
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.commons.util.FileUtil;
import javafx.application.Application;
-import seedu.address.commons.core.LogsCenter;
-import seedu.address.commons.util.FileUtil;
/**
* Represents the parsed command-line parameters given to the application.
diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/fasttrack/Main.java
similarity index 97%
rename from src/main/java/seedu/address/Main.java
rename to src/main/java/fasttrack/Main.java
index 052a5068631..6fdb0bb06e5 100644
--- a/src/main/java/seedu/address/Main.java
+++ b/src/main/java/fasttrack/Main.java
@@ -1,4 +1,4 @@
-package seedu.address;
+package fasttrack;
import javafx.application.Application;
diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/fasttrack/MainApp.java
similarity index 58%
rename from src/main/java/seedu/address/MainApp.java
rename to src/main/java/fasttrack/MainApp.java
index 4133aaa0151..cbaf8d9d8eb 100644
--- a/src/main/java/seedu/address/MainApp.java
+++ b/src/main/java/fasttrack/MainApp.java
@@ -1,54 +1,54 @@
-package seedu.address;
+package fasttrack;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.logging.Logger;
+import fasttrack.commons.core.Config;
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.commons.core.Version;
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.commons.util.ConfigUtil;
+import fasttrack.commons.util.StringUtil;
+import fasttrack.logic.Logic;
+import fasttrack.logic.LogicManager;
+import fasttrack.model.ExpenseTracker;
+import fasttrack.model.Model;
+import fasttrack.model.ModelManager;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.ReadOnlyUserPrefs;
+import fasttrack.model.UserPrefs;
+import fasttrack.model.util.SampleExpenseTracker;
+import fasttrack.storage.ExpenseTrackerStorage;
+import fasttrack.storage.JsonExpenseTrackerStorage;
+import fasttrack.storage.JsonUserPrefsStorage;
+import fasttrack.storage.Storage;
+import fasttrack.storage.StorageManager;
+import fasttrack.storage.UserPrefsStorage;
+import fasttrack.ui.Ui;
+import fasttrack.ui.UiManager;
import javafx.application.Application;
import javafx.stage.Stage;
-import seedu.address.commons.core.Config;
-import seedu.address.commons.core.LogsCenter;
-import seedu.address.commons.core.Version;
-import seedu.address.commons.exceptions.DataConversionException;
-import seedu.address.commons.util.ConfigUtil;
-import seedu.address.commons.util.StringUtil;
-import seedu.address.logic.Logic;
-import seedu.address.logic.LogicManager;
-import seedu.address.model.AddressBook;
-import seedu.address.model.Model;
-import seedu.address.model.ModelManager;
-import seedu.address.model.ReadOnlyAddressBook;
-import seedu.address.model.ReadOnlyUserPrefs;
-import seedu.address.model.UserPrefs;
-import seedu.address.model.util.SampleDataUtil;
-import seedu.address.storage.AddressBookStorage;
-import seedu.address.storage.JsonAddressBookStorage;
-import seedu.address.storage.JsonUserPrefsStorage;
-import seedu.address.storage.Storage;
-import seedu.address.storage.StorageManager;
-import seedu.address.storage.UserPrefsStorage;
-import seedu.address.ui.Ui;
-import seedu.address.ui.UiManager;
/**
* Runs the application.
*/
public class MainApp extends Application {
- public static final Version VERSION = new Version(0, 2, 0, true);
+ public static final Version VERSION = new Version(1, 4, 0, true);
private static final Logger logger = LogsCenter.getLogger(MainApp.class);
protected Ui ui;
protected Logic logic;
protected Storage storage;
- protected Model model;
+ protected Model dataModel;
protected Config config;
@Override
public void init() throws Exception {
- logger.info("=============================[ Initializing AddressBook ]===========================");
+ logger.info("=============================[ Initializing ExpenseTracker ]===========================");
super.init();
AppParameters appParameters = AppParameters.parse(getParameters());
@@ -56,39 +56,44 @@ 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);
+ ExpenseTrackerStorage expenseTrackerStorage = new JsonExpenseTrackerStorage(
+ userPrefs.getExpenseTrackerFilePath());
+ storage = new StorageManager(expenseTrackerStorage, userPrefsStorage);
initLogging(config);
- model = initModelManager(storage, userPrefs);
+ dataModel = initModelManager(storage, userPrefs);
- logic = new LogicManager(model, storage);
+ logic = new LogicManager(dataModel, storage);
ui = new UiManager(logic);
}
/**
- * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found,
- * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book.
+ * Returns a {@code ModelManager} with the data from {@code storage}'s expense
+ * tracker and {@code userPrefs}.
+ * The data from the sample expense tracker will be used instead if
+ * {@code storage}'s expense tracker is not found,
+ * or an empty expense tracker will be used instead if errors occur when reading
+ * {@code storage}'s expense tracker.
*/
private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) {
- Optional
addressBookOptional;
- ReadOnlyAddressBook initialData;
+ Optional expenseTrackerOptional;
+ ReadOnlyExpenseTracker initialData;
try {
- addressBookOptional = storage.readAddressBook();
- if (!addressBookOptional.isPresent()) {
- logger.info("Data file not found. Will be starting with a sample AddressBook");
+ expenseTrackerOptional = storage.readExpenseTracker();
+ if (!expenseTrackerOptional.isPresent()) {
+ logger.info("Data file not found. Will be starting with a sample ExpenseTracker");
}
- initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook);
+ initialData = expenseTrackerOptional.orElseGet(SampleExpenseTracker::getSampleExpenseTracker);
} 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 ExpenseTracker");
+ initialData = new ExpenseTracker();
} 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 ExpenseTracker");
+ initialData = new ExpenseTracker();
}
+ logger.info("fine");
return new ModelManager(initialData, userPrefs);
}
@@ -124,7 +129,8 @@ protected Config initConfig(Path configFilePath) {
initializedConfig = new Config();
}
- //Update config file in case it was missing to begin with or there are new/unused fields
+ // Update config file in case it was missing to begin with or there are
+ // new/unused fields
try {
ConfigUtil.saveConfig(initializedConfig, configFilePathUsed);
} catch (IOException e) {
@@ -134,7 +140,8 @@ protected Config initConfig(Path configFilePath) {
}
/**
- * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path,
+ * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs
+ * file path,
* or a new {@code UserPrefs} with default configuration if errors occur when
* reading from the file.
*/
@@ -151,11 +158,12 @@ 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 ExpenseTracker");
initializedPrefs = new UserPrefs();
}
- //Update prefs file in case it was missing to begin with or there are new/unused fields
+ // Update prefs file in case it was missing to begin with or there are
+ // new/unused fields
try {
storage.saveUserPrefs(initializedPrefs);
} catch (IOException e) {
@@ -167,17 +175,18 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) {
@Override
public void start(Stage primaryStage) {
- logger.info("Starting AddressBook " + MainApp.VERSION);
+ logger.info("Starting ExpenseTracker " + MainApp.VERSION);
ui.start(primaryStage);
}
@Override
public void stop() {
- logger.info("============================ [ Stopping Address Book ] =============================");
+ logger.info("============================ [ Stopping Expense Tracker ] =============================");
try {
- storage.saveUserPrefs(model.getUserPrefs());
+ storage.saveUserPrefs(dataModel.getUserPrefs());
+ storage.saveExpenseTracker(dataModel.getExpenseTracker());
} catch (IOException e) {
- logger.severe("Failed to save preferences " + StringUtil.getDetails(e));
+ logger.severe("Failed to save data " + StringUtil.getDetails(e));
}
}
}
diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/fasttrack/commons/core/Config.java
similarity index 97%
rename from src/main/java/seedu/address/commons/core/Config.java
rename to src/main/java/fasttrack/commons/core/Config.java
index 91145745521..c61972610aa 100644
--- a/src/main/java/seedu/address/commons/core/Config.java
+++ b/src/main/java/fasttrack/commons/core/Config.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.core;
+package fasttrack.commons.core;
import java.nio.file.Path;
import java.nio.file.Paths;
diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/fasttrack/commons/core/GuiSettings.java
similarity index 98%
rename from src/main/java/seedu/address/commons/core/GuiSettings.java
rename to src/main/java/fasttrack/commons/core/GuiSettings.java
index ba33653be67..48b5350237f 100644
--- a/src/main/java/seedu/address/commons/core/GuiSettings.java
+++ b/src/main/java/fasttrack/commons/core/GuiSettings.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.core;
+package fasttrack.commons.core;
import java.awt.Point;
import java.io.Serializable;
diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/fasttrack/commons/core/LogsCenter.java
similarity index 97%
rename from src/main/java/seedu/address/commons/core/LogsCenter.java
rename to src/main/java/fasttrack/commons/core/LogsCenter.java
index 431e7185e76..5a68d747bc5 100644
--- a/src/main/java/seedu/address/commons/core/LogsCenter.java
+++ b/src/main/java/fasttrack/commons/core/LogsCenter.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.core;
+package fasttrack.commons.core;
import java.io.IOException;
import java.util.Arrays;
@@ -18,7 +18,7 @@
public class LogsCenter {
private static final int MAX_FILE_COUNT = 5;
private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB
- private static final String LOG_FILE = "addressbook.log";
+ private static final String LOG_FILE = "fasttrack.log";
private static Level currentLogLevel = Level.INFO;
private static final Logger logger = LogsCenter.getLogger(LogsCenter.class);
private static FileHandler fileHandler;
diff --git a/src/main/java/fasttrack/commons/core/Messages.java b/src/main/java/fasttrack/commons/core/Messages.java
new file mode 100644
index 00000000000..5b4f7d90d66
--- /dev/null
+++ b/src/main/java/fasttrack/commons/core/Messages.java
@@ -0,0 +1,37 @@
+package fasttrack.commons.core;
+
+/**
+ * Container for user visible messages.
+ */
+public class Messages {
+
+ public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command";
+ public static final String MESSAGE_EXPENSES_LISTED_OVERVIEW = "%1$d expenses listed";
+
+ public static final String MESSAGE_INVALID_DATE_FORMAT = "Date should be of the form D/M/YY";
+
+ public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s";
+ public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid.";
+ public static final String MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX = "The category index provided is invalid!";
+ public static final String MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX = "The expense index provided is invalid!";
+ public static final String MESSAGE_INVALID_RECURRING_EXPENSE_DISPLAYED_INDEX = "The recurring expense index "
+ + "provided is invalid!";
+ public static final String MESSAGE_INVALID_EXPENSE_CATEGORY = "The provided category does not exist!";
+
+ public static final String MESSAGE_INVALID_EDIT_FOR_EXPENSE = "Please specify an edit to at least "
+ + "the date, name, price or category of the expense!";
+ public static final String MESSAGE_INVALID_EDIT_FOR_CATEGORIES = "Please specify an edit to at least "
+ + "the category name or the category's summary.";
+ public static final String MESSAGE_SUCCESSFULLY_EDITED_CATEGORY = "Edited category: %1$s";
+ public static final String MESSAGE_SUCCESSFULLY_EDITED_EXPENSE = "Edited expense: %1$s";
+ public static final String MESSAGE_SUCCESSFULLY_EDITED_RECURRING = "Edited recurring expense generator: %1$s";
+ public static final String MESSAGE_INVALID_ENUM_FOR_FREQUENCY = "The frequency provided is invalid!"
+ + "Please choose from the following: daily, weekly, monthly or yearly.";
+ public static final String MESSAGE_INVALID_CATEGORY_NAME = "Please provide a category name!";
+ public static final String MESSAGE_INVALID_EXPENSE_NAME = "Please provide an expense name!";
+ public static final String MESSAGE_ALREADY_EXISTING_CATEGORY = "This category name is already used!";
+ public static final String MESSAGE_INVALID_BUDGET = "Please provide a valid budget!";
+
+
+
+}
diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/fasttrack/commons/core/Version.java
similarity index 98%
rename from src/main/java/seedu/address/commons/core/Version.java
rename to src/main/java/fasttrack/commons/core/Version.java
index 12142ec1e32..6e5717a6e29 100644
--- a/src/main/java/seedu/address/commons/core/Version.java
+++ b/src/main/java/fasttrack/commons/core/Version.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.core;
+package fasttrack.commons.core;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/fasttrack/commons/core/index/Index.java
similarity index 97%
rename from src/main/java/seedu/address/commons/core/index/Index.java
rename to src/main/java/fasttrack/commons/core/index/Index.java
index 19536439c09..e8790792d74 100644
--- a/src/main/java/seedu/address/commons/core/index/Index.java
+++ b/src/main/java/fasttrack/commons/core/index/Index.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.core.index;
+package fasttrack.commons.core.index;
/**
* Represents a zero-based or one-based index.
diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/fasttrack/commons/exceptions/DataConversionException.java
similarity index 84%
rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java
rename to src/main/java/fasttrack/commons/exceptions/DataConversionException.java
index 1f689bd8e3f..9675779f614 100644
--- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java
+++ b/src/main/java/fasttrack/commons/exceptions/DataConversionException.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.exceptions;
+package fasttrack.commons.exceptions;
/**
* Represents an error during conversion of data from one format to another
diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/fasttrack/commons/exceptions/IllegalValueException.java
similarity index 93%
rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java
rename to src/main/java/fasttrack/commons/exceptions/IllegalValueException.java
index 19124db485c..b9d03c2d036 100644
--- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java
+++ b/src/main/java/fasttrack/commons/exceptions/IllegalValueException.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.exceptions;
+package fasttrack.commons.exceptions;
/**
* Signals that some given data does not fulfill some constraints.
diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/fasttrack/commons/util/AppUtil.java
similarity index 94%
rename from src/main/java/seedu/address/commons/util/AppUtil.java
rename to src/main/java/fasttrack/commons/util/AppUtil.java
index 87aa89c0326..202944205c6 100644
--- a/src/main/java/seedu/address/commons/util/AppUtil.java
+++ b/src/main/java/fasttrack/commons/util/AppUtil.java
@@ -1,9 +1,9 @@
-package seedu.address.commons.util;
+package fasttrack.commons.util;
import static java.util.Objects.requireNonNull;
+import fasttrack.MainApp;
import javafx.scene.image.Image;
-import seedu.address.MainApp;
/**
* A container for App specific utility functions
diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/fasttrack/commons/util/CollectionUtil.java
similarity index 96%
rename from src/main/java/seedu/address/commons/util/CollectionUtil.java
rename to src/main/java/fasttrack/commons/util/CollectionUtil.java
index eafe4dfd681..d0b57417e8a 100644
--- a/src/main/java/seedu/address/commons/util/CollectionUtil.java
+++ b/src/main/java/fasttrack/commons/util/CollectionUtil.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.util;
+package fasttrack.commons.util;
import static java.util.Objects.requireNonNull;
diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/fasttrack/commons/util/ConfigUtil.java
similarity index 77%
rename from src/main/java/seedu/address/commons/util/ConfigUtil.java
rename to src/main/java/fasttrack/commons/util/ConfigUtil.java
index f7f8a2bd44c..b9a2c31b14b 100644
--- a/src/main/java/seedu/address/commons/util/ConfigUtil.java
+++ b/src/main/java/fasttrack/commons/util/ConfigUtil.java
@@ -1,11 +1,11 @@
-package seedu.address.commons.util;
+package fasttrack.commons.util;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
-import seedu.address.commons.core.Config;
-import seedu.address.commons.exceptions.DataConversionException;
+import fasttrack.commons.core.Config;
+import fasttrack.commons.exceptions.DataConversionException;
/**
* A class for accessing the Config File.
diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/fasttrack/commons/util/FileUtil.java
similarity index 98%
rename from src/main/java/seedu/address/commons/util/FileUtil.java
rename to src/main/java/fasttrack/commons/util/FileUtil.java
index b1e2767cdd9..82d0415b43a 100644
--- a/src/main/java/seedu/address/commons/util/FileUtil.java
+++ b/src/main/java/fasttrack/commons/util/FileUtil.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.util;
+package fasttrack.commons.util;
import java.io.IOException;
import java.nio.file.Files;
diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/fasttrack/commons/util/JsonUtil.java
similarity index 97%
rename from src/main/java/seedu/address/commons/util/JsonUtil.java
rename to src/main/java/fasttrack/commons/util/JsonUtil.java
index 8ef609f055d..4eb6c54ea2c 100644
--- a/src/main/java/seedu/address/commons/util/JsonUtil.java
+++ b/src/main/java/fasttrack/commons/util/JsonUtil.java
@@ -1,4 +1,4 @@
-package seedu.address.commons.util;
+package fasttrack.commons.util;
import static java.util.Objects.requireNonNull;
@@ -20,8 +20,8 @@
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
-import seedu.address.commons.core.LogsCenter;
-import seedu.address.commons.exceptions.DataConversionException;
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.commons.exceptions.DataConversionException;
/**
* Converts a Java object instance to JSON and vice versa
diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/fasttrack/commons/util/StringUtil.java
similarity index 84%
rename from src/main/java/seedu/address/commons/util/StringUtil.java
rename to src/main/java/fasttrack/commons/util/StringUtil.java
index 61cc8c9a1cb..88298dfddc5 100644
--- a/src/main/java/seedu/address/commons/util/StringUtil.java
+++ b/src/main/java/fasttrack/commons/util/StringUtil.java
@@ -1,7 +1,7 @@
-package seedu.address.commons.util;
+package fasttrack.commons.util;
+import static fasttrack.commons.util.AppUtil.checkArgument;
import static java.util.Objects.requireNonNull;
-import static seedu.address.commons.util.AppUtil.checkArgument;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -65,4 +65,16 @@ public static boolean isNonZeroUnsignedInteger(String s) {
return false;
}
}
+
+ /**
+ * Returns true if {@code s} represents a valid string for a field
+ */
+ public static boolean isValidField(String s) {
+ if (s == null) {
+ return false;
+ }
+ // Regex to strip extra whitespace between words
+ s = s.replaceAll("\\s+", " ");
+ return s.matches("[\\p{Alnum}][\\p{Alnum} ]*");
+ }
}
diff --git a/src/main/java/fasttrack/logic/Logic.java b/src/main/java/fasttrack/logic/Logic.java
new file mode 100644
index 00000000000..79be168e08f
--- /dev/null
+++ b/src/main/java/fasttrack/logic/Logic.java
@@ -0,0 +1,64 @@
+package fasttrack.logic;
+
+import java.nio.file.Path;
+
+import fasttrack.commons.core.GuiSettings;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.Model;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+
+/**
+ * API of the Logic component
+ */
+public interface Logic {
+ /**
+ * Executes the command and returns the result.
+ * @param commandText The command as entered by the user.
+ * @return the result of the command execution.
+ * @throws CommandException If an error occurs during command execution.
+ * @throws ParseException If an error occurs during parsing.
+ */
+ CommandResult execute(String commandText) throws CommandException, ParseException;
+
+ /**
+ * Returns the ExpenseTracker.
+ * @see Model#getExpenseTracker()
+ */
+ ReadOnlyExpenseTracker getExpenseTracker();
+
+ /** Returns an unmodifiable view of the list of categories */
+ ObservableList getFilteredCategoryList();
+
+ /** Returns an unmodifiable view of the filtered list of expenses */
+ ObservableList getFilteredExpenseList();
+
+ /** Returns an unmodifiable view of the list of recurring expenses */
+ ObservableList getRecurringExpenseManagerList();
+
+ SimpleObjectProperty getAppliedTimeSpanFilter();
+
+ SimpleObjectProperty getAppliedCategoryFilter();
+
+ /**
+ * Returns the user prefs' file path.
+ */
+ Path getAddressBookFilePath();
+
+ /**
+ * Returns the user prefs' GUI settings.
+ */
+ GuiSettings getGuiSettings();
+
+ /**
+ * Set the user prefs' GUI settings.
+ */
+ void setGuiSettings(GuiSettings guiSettings);
+}
diff --git a/src/main/java/fasttrack/logic/LogicManager.java b/src/main/java/fasttrack/logic/LogicManager.java
new file mode 100644
index 00000000000..d3e0290379b
--- /dev/null
+++ b/src/main/java/fasttrack/logic/LogicManager.java
@@ -0,0 +1,106 @@
+package fasttrack.logic;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+
+import fasttrack.commons.core.GuiSettings;
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.logic.commands.Command;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.logic.parser.ExpenseTrackerParser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.Model;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.storage.Storage;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+
+/**
+ * The main LogicManager of the app.
+ */
+public class LogicManager implements Logic {
+ public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: ";
+ private final Logger logger = LogsCenter.getLogger(LogicManager.class);
+
+ private final Model dataModel;
+ private final Storage storage;
+ private final ExpenseTrackerParser expenseTrackerParser;
+
+ /**
+ * Constructs a {@code LogicManager} with the given {@code Model} and
+ * {@code Storage}.
+ */
+ public LogicManager(Model dataModel, Storage storage) {
+ this.dataModel = dataModel;
+ this.storage = storage;
+ expenseTrackerParser = new ExpenseTrackerParser();
+ }
+
+ @Override
+ public CommandResult execute(String commandText) throws CommandException, ParseException {
+ logger.info("----------------[USER COMMAND][" + commandText + "]");
+ CommandResult commandResult;
+ Command command = expenseTrackerParser.parseCommand(commandText);
+ commandResult = command.execute(dataModel);
+
+ try {
+ storage.saveExpenseTracker(dataModel.getExpenseTracker());
+ } catch (IOException ioe) {
+ throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe);
+ }
+
+ return commandResult;
+ }
+
+ @Override
+ public ReadOnlyExpenseTracker getExpenseTracker() {
+ return dataModel.getExpenseTracker();
+ }
+
+ @Override
+ public ObservableList getFilteredCategoryList() {
+ return dataModel.getFilteredCategoryList();
+ }
+
+ @Override
+ public ObservableList getFilteredExpenseList() {
+ return dataModel.getFilteredExpenseList();
+ }
+
+ @Override
+ public ObservableList getRecurringExpenseManagerList() {
+ return dataModel.getRecurringExpenseGenerators();
+ }
+
+ @Override
+ public SimpleObjectProperty getAppliedTimeSpanFilter() {
+ return dataModel.getAppliedTimeSpanFilter();
+ }
+
+ @Override
+ public SimpleObjectProperty getAppliedCategoryFilter() {
+ return dataModel.getAppliedCategoryFilter();
+ }
+
+ @Override
+ public Path getAddressBookFilePath() {
+ return dataModel.getExpenseTrackerFilePath();
+ }
+
+
+ @Override
+ public GuiSettings getGuiSettings() {
+ return dataModel.getGuiSettings();
+ }
+
+ @Override
+ public void setGuiSettings(GuiSettings guiSettings) {
+ dataModel.setGuiSettings(guiSettings);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/fasttrack/logic/commands/Command.java
similarity index 50%
rename from src/main/java/seedu/address/logic/commands/Command.java
rename to src/main/java/fasttrack/logic/commands/Command.java
index 64f18992160..8301310f285 100644
--- a/src/main/java/seedu/address/logic/commands/Command.java
+++ b/src/main/java/fasttrack/logic/commands/Command.java
@@ -1,20 +1,20 @@
-package seedu.address.logic.commands;
+package fasttrack.logic.commands;
-import seedu.address.logic.commands.exceptions.CommandException;
-import seedu.address.model.Model;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
/**
* Represents a command with hidden internal logic and the ability to be executed.
*/
-public abstract class Command {
+public interface Command {
/**
* Executes the command and returns the result message.
*
- * @param model {@code Model} which the command should operate on.
+ * @param dataModel {@code Model} which the command should operate on.
* @return feedback message of the operation result for display
* @throws CommandException If an error occurs during command execution.
*/
- public abstract CommandResult execute(Model model) throws CommandException;
+ public abstract CommandResult execute(Model dataModel) throws CommandException;
}
diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/fasttrack/logic/commands/CommandResult.java
similarity index 52%
rename from src/main/java/seedu/address/logic/commands/CommandResult.java
rename to src/main/java/fasttrack/logic/commands/CommandResult.java
index 92f900b7916..66005d0353d 100644
--- a/src/main/java/seedu/address/logic/commands/CommandResult.java
+++ b/src/main/java/fasttrack/logic/commands/CommandResult.java
@@ -1,9 +1,11 @@
-package seedu.address.logic.commands;
+package fasttrack.logic.commands;
import static java.util.Objects.requireNonNull;
import java.util.Objects;
+import fasttrack.ui.ScreenType;
+
/**
* Represents the result of a command execution.
*/
@@ -17,21 +19,25 @@ public class CommandResult {
/** The application should exit. */
private final boolean exit;
+ /** The screen to display upon execution of the command. */
+ private final ScreenType screenType;
+
/**
* Constructs a {@code CommandResult} with the specified fields.
*/
- public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) {
+ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, ScreenType screenType) {
this.feedbackToUser = requireNonNull(feedbackToUser);
this.showHelp = showHelp;
this.exit = exit;
+ this.screenType = screenType;
}
/**
* Constructs a {@code CommandResult} with the specified {@code feedbackToUser},
* and other fields set to their default value.
*/
- public CommandResult(String feedbackToUser) {
- this(feedbackToUser, false, false);
+ public CommandResult(String feedbackToUser, ScreenType screenType) {
+ this(feedbackToUser, false, false, screenType);
}
public String getFeedbackToUser() {
@@ -46,21 +52,17 @@ public boolean isExit() {
return exit;
}
+ public ScreenType getScreenType() {
+ return screenType;
+ }
+
@Override
public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
-
- // instanceof handles nulls
- if (!(other instanceof CommandResult)) {
- return false;
- }
-
- CommandResult otherCommandResult = (CommandResult) other;
- return feedbackToUser.equals(otherCommandResult.feedbackToUser)
- && showHelp == otherCommandResult.showHelp
- && exit == otherCommandResult.exit;
+ return other == this // short circuit if same object
+ || (other instanceof CommandResult // instanceof handles nulls
+ && feedbackToUser.equals(((CommandResult) other).feedbackToUser)
+ && showHelp == ((CommandResult) other).showHelp
+ && exit == ((CommandResult) other).exit);
}
@Override
@@ -68,4 +70,14 @@ public int hashCode() {
return Objects.hash(feedbackToUser, showHelp, exit);
}
+ @Override
+ public String toString() {
+ return "CommandResult{"
+ + "feedbackToUser='" + feedbackToUser + '\''
+ + ", showHelp=" + showHelp
+ + ", exit=" + exit
+ + ", screenType=" + screenType
+ + '}';
+ }
+
}
diff --git a/src/main/java/fasttrack/logic/commands/SetBudgetCommand.java b/src/main/java/fasttrack/logic/commands/SetBudgetCommand.java
new file mode 100644
index 00000000000..18b06c4b65e
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/SetBudgetCommand.java
@@ -0,0 +1,51 @@
+package fasttrack.logic.commands;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+
+import fasttrack.model.Budget;
+import fasttrack.model.Model;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Sets the monthly budget for FastTrack.
+ */
+public class SetBudgetCommand implements Command {
+
+
+ public static final String COMMAND_WORD = "set";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sets monthly budget\n"
+ + "Parameters: "
+ + PREFIX_PRICE + "BUDGET_AMOUNT\n"
+ + "Example: " + COMMAND_WORD + " " + PREFIX_PRICE + "1000";
+
+ public static final String MESSAGE_SUCCESS = "Monthly budget successfully set to ";
+ private final Budget budget;
+
+ public SetBudgetCommand(Budget budget) {
+ this.budget = budget;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) {
+ dataModel.setBudget(budget);
+ return new CommandResult(MESSAGE_SUCCESS + this.budget, ScreenType.EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof SetBudgetCommand // instanceof handles nulls
+ && budget.equals(((SetBudgetCommand) other).budget));
+ }
+
+ @Override
+ public String toString() {
+ return "SetBudgetCommand{budget=" + budget + '}';
+ }
+
+ @Override
+ public int hashCode() {
+ return budget.hashCode();
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/add/AddCategoryCommand.java b/src/main/java/fasttrack/logic/commands/add/AddCategoryCommand.java
new file mode 100644
index 00000000000..b5fd20a346e
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/add/AddCategoryCommand.java
@@ -0,0 +1,55 @@
+package fasttrack.logic.commands.add;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY;
+import static java.util.Objects.requireNonNull;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Adds a category to the Expense Tracker.
+ */
+public class AddCategoryCommand implements AddCommand {
+
+ public static final String COMMAND_WORD = "addcat";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a category to FastTrack. "
+ + "Parameters: "
+ + PREFIX_CATEGORY + "CATEGORY_NAME "
+ + "[" + PREFIX_SUMMARY + "CATEGORY_SUMMARY]\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_CATEGORY + "groceries "
+ + PREFIX_SUMMARY + "all expenses related to groceries\n";
+ public static final String MESSAGE_SUCCESS = "New category added: %1$s";
+ public static final String MESSAGE_DUPLICATE_CATEGORY = "This category already exists in FastTrack";
+
+ private final Category toAdd;
+
+ /**
+ * Creates an AddCategoryCommand to add the specified {@code Category}
+ */
+ public AddCategoryCommand(Category category) {
+ requireNonNull(category);
+ toAdd = category;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ if (dataModel.hasCategory(toAdd)) {
+ throw new CommandException(MESSAGE_DUPLICATE_CATEGORY);
+ }
+ dataModel.addCategory(toAdd);
+ return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), ScreenType.CATEGORY_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof AddCategoryCommand // instanceof handles nulls
+ && toAdd.equals(((AddCategoryCommand) other).toAdd));
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/add/AddCommand.java b/src/main/java/fasttrack/logic/commands/add/AddCommand.java
new file mode 100644
index 00000000000..7430248e9f7
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/add/AddCommand.java
@@ -0,0 +1,9 @@
+package fasttrack.logic.commands.add;
+
+import fasttrack.logic.commands.Command;
+
+/**
+ * Represents an add command with hidden internal logic and the ability to be
+ * executed.
+ */
+public interface AddCommand extends Command {}
diff --git a/src/main/java/fasttrack/logic/commands/add/AddExpenseCommand.java b/src/main/java/fasttrack/logic/commands/add/AddExpenseCommand.java
new file mode 100644
index 00000000000..200fe0e1300
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/add/AddExpenseCommand.java
@@ -0,0 +1,67 @@
+package fasttrack.logic.commands.add;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+import static java.util.Objects.requireNonNull;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.ui.ScreenType;
+
+
+/**
+ * Adds an expense to the Expense Tracker.
+ */
+public class AddExpenseCommand implements AddCommand {
+
+ public static final String COMMAND_WORD = "add";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an expense to FastTrack "
+ + "Parameters: "
+ + PREFIX_NAME + "EXPENSE NAME "
+ + PREFIX_CATEGORY + "CATEGORY "
+ + PREFIX_PRICE + "AMOUNT "
+ + "[" + PREFIX_DATE + "DATE]\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_NAME + "Milk "
+ + PREFIX_CATEGORY + "groceries "
+ + PREFIX_PRICE + "4.50 "
+ + PREFIX_DATE + "2/10/23";
+
+ public static final String MESSAGE_SUCCESS = "New expense added: %1$s";
+
+ private final Expense newExpense;
+
+ /**
+ * Creates an AddExpenseCommand to add the specified {@code Expense}
+ */
+ public AddExpenseCommand(Expense expense) {
+ requireNonNull(expense);
+ newExpense = expense;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ Category newCategory = newExpense.getCategory();
+ Category existingCategory = dataModel.getCategoryInstance(newCategory);
+ if (existingCategory != null) {
+ newExpense.setCategory(existingCategory);
+ } else {
+ dataModel.addCategory(newCategory);
+ }
+ dataModel.addExpense(newExpense);
+ return new CommandResult(String.format(MESSAGE_SUCCESS, newExpense), ScreenType.EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof AddExpenseCommand // instanceof handles nulls
+ && newExpense.equals(((AddExpenseCommand) other).newExpense));
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/add/AddRecurringExpenseCommand.java b/src/main/java/fasttrack/logic/commands/add/AddRecurringExpenseCommand.java
new file mode 100644
index 00000000000..a94bbf4613a
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/add/AddRecurringExpenseCommand.java
@@ -0,0 +1,79 @@
+package fasttrack.logic.commands.add;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_START_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN;
+import static java.util.Objects.requireNonNull;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Adds a category to the Expense Tracker.
+ */
+public class AddRecurringExpenseCommand implements AddCommand {
+
+ public static final String COMMAND_WORD = "addrec";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a recurring expense to FastTrack. "
+ + "Parameters: "
+ + PREFIX_NAME + "RECURRING_EXPENSE_NAME"
+ + PREFIX_CATEGORY + "CATEGORY_NAME "
+ + PREFIX_PRICE + "AMOUNT "
+ + PREFIX_TIMESPAN + "TIMESPAN "
+ + PREFIX_START_DATE + "START_DATE "
+ + "[" + PREFIX_END_DATE + "END_DATE]\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_NAME + "Netflix "
+ + PREFIX_CATEGORY + "Subscription "
+ + PREFIX_PRICE + "10 "
+ + PREFIX_TIMESPAN + "week "
+ + PREFIX_START_DATE + "01/03/23 "
+ + PREFIX_END_DATE + "01/03/24\n";
+ public static final String MESSAGE_SUCCESS = "New recurring expense added: %1$s";
+ public static final String MESSAGE_DUPLICATE_RECURRING_EXPENSE =
+ "This recurring expense already exists in FastTrack";
+
+ private final RecurringExpenseManager toAdd;
+
+ /**
+ * Creates an AddCategoryCommand to add the specified {@code Category}
+ */
+ public AddRecurringExpenseCommand(RecurringExpenseManager toAdd) {
+ requireNonNull(toAdd);
+ this.toAdd = toAdd;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ if (dataModel.hasRecurringExpense(toAdd)) {
+ throw new CommandException(MESSAGE_DUPLICATE_RECURRING_EXPENSE);
+ }
+
+ Category newCategory = toAdd.getExpenseCategory();
+ Category existingCategory = dataModel.getCategoryInstance(newCategory);
+ if (existingCategory != null) {
+ toAdd.setExpenseCategory(existingCategory);
+ } else {
+ dataModel.addCategory(newCategory);
+ }
+
+ dataModel.addRecurringGenerator(toAdd);
+ dataModel.addRetroactiveExpenses();
+ return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), ScreenType.RECURRING_EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof AddRecurringExpenseCommand // instanceof handles nulls
+ && toAdd.equals(((AddRecurringExpenseCommand) other).toAdd));
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteCategoryCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteCategoryCommand.java
new file mode 100644
index 00000000000..5eaea084a86
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/delete/DeleteCategoryCommand.java
@@ -0,0 +1,61 @@
+package fasttrack.logic.commands.delete;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Deletes a category identified using it's displayed index from the expense tracker.
+ */
+public class DeleteCategoryCommand implements DeleteCommand {
+
+ public static final String COMMAND_WORD = "delcat";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Deletes the category identified by the index number used in the displayed category list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+
+ public static final String MESSAGE_DELETE_CATEGORY_SUCCESS = "Deleted category: %1$s";
+
+ private final Index targetIndex;
+
+ /**
+ * Creates an DeleteCategory to delete the specified {@code Category}
+ * @param targetIndex index of the category in the filtered category list to delete
+ */
+ public DeleteCategoryCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ List lastShownList = dataModel.getFilteredCategoryList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX);
+ }
+
+ Category categoryToDelete = lastShownList.get(targetIndex.getZeroBased());
+ dataModel.deleteCategory(categoryToDelete);
+ return new CommandResult(
+ String.format(MESSAGE_DELETE_CATEGORY_SUCCESS, categoryToDelete),
+ ScreenType.CATEGORY_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof DeleteCategoryCommand // instanceof handles nulls
+ && targetIndex.equals(((DeleteCategoryCommand) other).targetIndex)); // state check
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteCommand.java
new file mode 100644
index 00000000000..0d8fbc2fd42
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/delete/DeleteCommand.java
@@ -0,0 +1,9 @@
+package fasttrack.logic.commands.delete;
+
+import fasttrack.logic.commands.Command;
+
+/**
+ * Represents a delete command with hidden internal logic and the ability to be executed.
+ */
+public interface DeleteCommand extends Command {
+}
diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteExpenseCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteExpenseCommand.java
new file mode 100644
index 00000000000..7a8b7fddf68
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/delete/DeleteExpenseCommand.java
@@ -0,0 +1,57 @@
+package fasttrack.logic.commands.delete;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.expense.Expense;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Deletes an expense from the expense tracker.
+ */
+public class DeleteExpenseCommand implements DeleteCommand {
+
+ public static final String COMMAND_WORD = "delete";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Deletes the expense identified by the index number used in the displayed expenses list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+
+ public static final String MESSAGE_DELETE_EXPENSE_SUCCESS = "Deleted expense: %1$s";
+ private final Index targetIndex;
+ /**
+ * Creates an DeleteExpenseCommand to delete the specified {@code Expense}
+ * @param targetIndex index of the expense in the filtered expense list to delete
+ */
+ public DeleteExpenseCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ List lastShownList = dataModel.getFilteredExpenseList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_INDEX);
+ }
+
+ Expense expense = lastShownList.get(targetIndex.getZeroBased());
+ dataModel.deleteExpense(expense);
+ return new CommandResult(String.format(MESSAGE_DELETE_EXPENSE_SUCCESS, expense), ScreenType.EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof DeleteExpenseCommand // instanceof handles nulls
+ && targetIndex.equals(((DeleteExpenseCommand) other).targetIndex)); // state check
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommand.java
new file mode 100644
index 00000000000..a4b8db7c72e
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommand.java
@@ -0,0 +1,59 @@
+package fasttrack.logic.commands.delete;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Deletes an expense from the expense tracker.
+ */
+public class DeleteRecurringExpenseCommand implements DeleteCommand {
+
+ public static final String COMMAND_WORD = "delrec";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Deletes the recurring expense identified by the index number used in the displayed "
+ + "recurring expenses list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+
+ public static final String MESSAGE_DELETE_RECURRING_EXPENSE_SUCCESS = "Deleted recurring expense: %1$s";
+ private final Index targetIndex;
+ /**
+ * Creates an DeleteExpenseCommand to delete the specified {@code Expense}
+ * @param targetIndex index of the expense in the filtered expense list to delete
+ */
+ public DeleteRecurringExpenseCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ List lastShownList = dataModel.getRecurringExpenseGenerators();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_RECURRING_EXPENSE_DISPLAYED_INDEX);
+ }
+
+ RecurringExpenseManager recurringExpenseManager = lastShownList.get(targetIndex.getZeroBased());
+ dataModel.deleteRecurringExpense(recurringExpenseManager);
+ return new CommandResult(String.format(MESSAGE_DELETE_RECURRING_EXPENSE_SUCCESS, recurringExpenseManager),
+ ScreenType.RECURRING_EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof DeleteRecurringExpenseCommand // instanceof handles nulls
+ && targetIndex.equals(((DeleteRecurringExpenseCommand) other).targetIndex)); // state check
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/edit/EditCategoryCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditCategoryCommand.java
new file mode 100644
index 00000000000..346071db623
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/edit/EditCategoryCommand.java
@@ -0,0 +1,96 @@
+package fasttrack.logic.commands.edit;
+
+import static fasttrack.logic.commands.add.AddCategoryCommand.MESSAGE_DUPLICATE_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY;
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.model.category.UserDefinedCategory;
+import fasttrack.ui.ScreenType;
+
+
+/**
+ * Edits a category in the ExpenseTracker
+ */
+public class EditCategoryCommand implements EditCommand {
+ public static final String COMMAND_WORD = "edcat";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Edits the category identified by the index number used in the displayed category list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "[" + PREFIX_CATEGORY + "CATEGORY] "
+ + "[" + PREFIX_SUMMARY + "SUMMARY] "
+ + "Example: " + COMMAND_WORD + " edcat 1 c/food s/for meals";
+
+ private final Index targetIndex;
+
+ private final String newCategoryName;
+
+ private final String newSummary;
+
+ /**
+ * Creates an EditCategory to edit the specified {@code Category}
+ * @param targetIndex index of the expense in the filtered category list to edit.
+ * @param newCategoryName String representation of the new category name to be edited to, if applicable.
+ * @param newSummary String representation of the new summary to be edited to, if applicable.
+ */
+ public EditCategoryCommand(Index targetIndex, String newCategoryName, String newSummary) {
+ requireNonNull(targetIndex);
+ this.targetIndex = targetIndex;
+ this.newCategoryName = newCategoryName;
+ this.newSummary = newSummary;
+ }
+
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredCategoryList();
+
+ //Check whether targetIndex is a valid index.
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX);
+ }
+ UserDefinedCategory categoryToEdit = (UserDefinedCategory) lastShownList.get(targetIndex.getZeroBased());
+
+ if (newCategoryName != null) {
+
+ if (newCategoryName.isBlank()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_NAME);
+ }
+ for (Category category : lastShownList) {
+ if (category.getCategoryName().equalsIgnoreCase(newCategoryName)) {
+ throw new CommandException(MESSAGE_DUPLICATE_CATEGORY);
+ }
+ }
+ categoryToEdit.setCategoryName(newCategoryName.replaceAll("\\s+", " "));
+ }
+ if (newSummary != null) {
+ categoryToEdit.setDescription(newSummary.replaceAll("\\s+", " "));
+ }
+
+ if (newCategoryName == null && newSummary == null) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EDIT_FOR_CATEGORIES);
+ }
+
+ return new CommandResult(
+ String.format(Messages.MESSAGE_SUCCESSFULLY_EDITED_CATEGORY,
+ categoryToEdit), ScreenType.CATEGORY_SCREEN);
+
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof EditCategoryCommand // instanceof handles nulls
+ && targetIndex.equals(((EditCategoryCommand) other).targetIndex)); // state check
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/edit/EditCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditCommand.java
new file mode 100644
index 00000000000..cf25e8cb821
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/edit/EditCommand.java
@@ -0,0 +1,11 @@
+package fasttrack.logic.commands.edit;
+
+import fasttrack.logic.commands.Command;
+
+/**
+ * Represents a edit command with hidden internal logic and the ability to be
+ * executed.
+ */
+public interface EditCommand extends Command {
+
+}
diff --git a/src/main/java/fasttrack/logic/commands/edit/EditExpenseCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditExpenseCommand.java
new file mode 100644
index 00000000000..881a8ad6261
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/edit/EditExpenseCommand.java
@@ -0,0 +1,138 @@
+package fasttrack.logic.commands.edit;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+import static java.util.Objects.requireNonNull;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Objects;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.Price;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Edits an Expense in the expense tracker.
+ */
+public class EditExpenseCommand implements EditCommand {
+ public static final String COMMAND_WORD = "edexp";
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Edits the expense identified by the index number used in the displayed expenses list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "[" + PREFIX_NAME + "EXPENSE NAME] "
+ + "[" + PREFIX_CATEGORY + "CATEGORY] "
+ + "[" + PREFIX_PRICE + "AMOUNT] "
+ + "[" + PREFIX_DATE + "DATE] \n"
+ + "Example: " + COMMAND_WORD + " 1 n/KFC c/food p/10 d/20/03/23 ";
+
+ private final Index targetIndex;
+ private final String newExpenseName;
+ private final Double newExpenseAmount;
+ private final LocalDate newExpenseDate;
+ private final String newExpenseCategoryInString;
+
+
+ /**
+ * JavaDoc
+ * @param targetIndex xx
+ * @param newExpenseName xx
+ * @param newExpenseAmount xx
+ * @param newExpenseDate xx
+ * @param newExpenseCategory xx
+ */
+ public EditExpenseCommand(Index targetIndex, String newExpenseName, Double newExpenseAmount,
+ LocalDate newExpenseDate, String newExpenseCategory) {
+ requireNonNull(targetIndex);
+ this.targetIndex = targetIndex;
+ this.newExpenseName = newExpenseName;
+ this.newExpenseAmount = newExpenseAmount;
+ this.newExpenseDate = newExpenseDate;
+ this.newExpenseCategoryInString = newExpenseCategory;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownListOfExpenses = model.getFilteredExpenseList();
+ List lastShownListOfCategories = model.getFilteredCategoryList();
+ Category toBeAllocated = null;
+
+ for (Category category : lastShownListOfCategories) {
+ if (category.getCategoryName().equalsIgnoreCase(this.newExpenseCategoryInString)) {
+ toBeAllocated = category;
+ break;
+ }
+ }
+
+ if (newExpenseName == null && newExpenseAmount == null
+ && newExpenseDate == null && newExpenseCategoryInString == null) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EDIT_FOR_EXPENSE);
+ }
+
+ if (targetIndex.getZeroBased() >= lastShownListOfExpenses.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX);
+ }
+
+ Expense expenseToEdit = lastShownListOfExpenses.get(targetIndex.getZeroBased());
+ String name = expenseToEdit.getName();
+ double amount = expenseToEdit.getAmount();
+
+ Category category = expenseToEdit.getCategory();
+ LocalDate date = expenseToEdit.getDate();
+
+ if (toBeAllocated != null) {
+ category = toBeAllocated;
+ } else if (this.newExpenseCategoryInString != null) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_CATEGORY);
+ }
+
+ if (newExpenseName != null) {
+ if (newExpenseName.stripTrailing().isEmpty()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_NAME);
+ }
+ name = newExpenseName;
+ }
+
+ if (newExpenseAmount != null) {
+ if (!Price.isValidPrice(String.valueOf(newExpenseAmount))) {
+ throw new CommandException(Price.MESSAGE_CONSTRAINTS);
+ }
+ amount = newExpenseAmount;
+ }
+
+ if (newExpenseDate != null) {
+ date = newExpenseDate;
+ }
+
+ Expense newExpense = new Expense(name, amount, date, category);
+ model.setExpense(expenseToEdit, newExpense);
+ return new CommandResult(
+ String.format(Messages.MESSAGE_SUCCESSFULLY_EDITED_EXPENSE, newExpense),
+ ScreenType.EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof EditExpenseCommand)) {
+ return false;
+ }
+ EditExpenseCommand otherTypeCasted = (EditExpenseCommand) other;
+ return targetIndex.equals(otherTypeCasted.targetIndex)
+ && (Objects.equals(newExpenseName, otherTypeCasted.newExpenseName))
+ && (Objects.equals(newExpenseAmount, otherTypeCasted.newExpenseAmount))
+ && (Objects.equals(newExpenseDate, otherTypeCasted.newExpenseDate))
+ && (Objects.equals(newExpenseCategoryInString, otherTypeCasted.newExpenseCategoryInString));
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommand.java
new file mode 100644
index 00000000000..f3d9231e641
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommand.java
@@ -0,0 +1,154 @@
+package fasttrack.logic.commands.edit;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN;
+import static java.util.Objects.requireNonNull;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Objects;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Price;
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.model.expense.RecurringExpenseType;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Edits a RecurringExpenseManager in the ExpenseTracker
+ */
+public class EditRecurringExpenseManagerCommand implements EditCommand {
+ public static final String COMMAND_WORD = "edrec";
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Edits the recurring expense identified by the index number used in the displayed recurring"
+ + " expenses list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "[" + PREFIX_NAME + "EXPENSE NAME] "
+ + "[" + PREFIX_CATEGORY + "CATEGORY] "
+ + "[" + PREFIX_PRICE + "AMOUNT] "
+ + "[" + PREFIX_TIMESPAN + "FREQUENCY] \n"
+ + "[" + PREFIX_END_DATE + "END-DATE] \n"
+ + "Example: " + COMMAND_WORD + " 1 n/KFC c/food p/10 t/weekly ed/20/03/23 ";
+
+ private final Index targetIndex;
+ private final String newExpenseName;
+ private final String newExpenseCategoryInString;
+ private final Double newExpenseAmount;
+ private final String newFrequencyInString;
+ private final LocalDate newExpenseEndDate;
+
+ /**
+ * Creates an EditRecurringExpenseManagerCommand to edit the specified
+ * {@code RecurringExpenseManager}
+ * @param targetIndex index of the recurringexpensemanager in the
+ * filtered recurringexpensemanager list to
+ * edit.
+ * @param newExpenseName String representation of the new category
+ * name to be edited to, if applicable.
+ * @param newExpenseAmount New expense price to be edited to, if
+ * applicable.
+ * @param newExpenseCategoryInString String representation of the new category's
+ * name to be edited to,
+ * if applicable.
+ * @param newFrequencyInString String representation of the frequency to
+ * be edited to, if applicable.
+ * @param newExpenseEndDate New recurring expense end date to be edited
+ * to, if applicable.
+ */
+ public EditRecurringExpenseManagerCommand(Index targetIndex, String newExpenseName, Double newExpenseAmount,
+ String newExpenseCategoryInString, String newFrequencyInString,
+ LocalDate newExpenseEndDate) {
+ requireNonNull(targetIndex);
+ this.targetIndex = targetIndex;
+ this.newExpenseName = newExpenseName;
+ this.newExpenseAmount = newExpenseAmount;
+ this.newExpenseCategoryInString = newExpenseCategoryInString;
+ this.newFrequencyInString = newFrequencyInString;
+ this.newExpenseEndDate = newExpenseEndDate;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownListOfCategories = model.getFilteredCategoryList();
+ List lastShownListOfRecurringExpenseManagers = model.getRecurringExpenseGenerators();
+ Category toBeAllocated = null;
+
+ for (Category category : lastShownListOfCategories) {
+ if (category.getCategoryName().equalsIgnoreCase(this.newExpenseCategoryInString)) {
+ toBeAllocated = category;
+ break;
+ }
+ }
+
+ if (newExpenseName == null && newExpenseAmount == null
+ && newFrequencyInString == null && newExpenseCategoryInString == null
+ && this.newExpenseEndDate == null) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EDIT_FOR_EXPENSE);
+ }
+
+ // Check if index is valid
+ if (targetIndex == null || targetIndex.getZeroBased() >= lastShownListOfRecurringExpenseManagers.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX);
+ }
+
+ RecurringExpenseManager generatorToEdit = lastShownListOfRecurringExpenseManagers
+ .get(targetIndex.getZeroBased());
+
+ if (toBeAllocated != null) {
+ generatorToEdit.setExpenseCategory(toBeAllocated);
+ } else if (this.newExpenseCategoryInString != null) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_CATEGORY);
+ }
+
+ if (newExpenseName != null) {
+ if (newExpenseName.stripTrailing().isEmpty()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_NAME);
+ }
+ generatorToEdit.setExpenseName(newExpenseName);
+ }
+
+ if (newExpenseAmount != null) {
+ if (!Price.isValidPrice(String.valueOf(newExpenseAmount))) {
+ throw new CommandException(Price.MESSAGE_CONSTRAINTS);
+ }
+ generatorToEdit.setAmount(newExpenseAmount);
+ }
+
+ if (newExpenseEndDate != null) {
+ generatorToEdit.setEndDate(newExpenseEndDate);
+ }
+
+ if (newFrequencyInString != null) {
+ // Check if it belongs in the enum
+ try {
+ RecurringExpenseType newTypeToSet = RecurringExpenseType.valueOf(newFrequencyInString);
+ generatorToEdit.setRecurringExpenseType(newTypeToSet);
+ } catch (IllegalArgumentException iae) {
+ throw new CommandException(Messages.MESSAGE_INVALID_ENUM_FOR_FREQUENCY);
+ }
+ }
+ return new CommandResult(
+ String.format(Messages.MESSAGE_SUCCESSFULLY_EDITED_RECURRING, generatorToEdit),
+ ScreenType.RECURRING_EXPENSE_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ EditRecurringExpenseManagerCommand otherTypeCasted = (EditRecurringExpenseManagerCommand) other;
+ return targetIndex.equals(otherTypeCasted.targetIndex)
+ && (Objects.equals(newExpenseName, otherTypeCasted.newExpenseName))
+ && (Objects.equals(newExpenseAmount, otherTypeCasted.newExpenseAmount))
+ && (Objects.equals(newExpenseEndDate, otherTypeCasted.newExpenseEndDate))
+ && (Objects.equals(newExpenseCategoryInString, otherTypeCasted.newExpenseCategoryInString))
+ && (Objects.equals(newFrequencyInString, otherTypeCasted.newFrequencyInString));
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/fasttrack/logic/commands/exceptions/CommandException.java
similarity index 89%
rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java
rename to src/main/java/fasttrack/logic/commands/exceptions/CommandException.java
index a16bd14f2cd..9f71527929f 100644
--- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java
+++ b/src/main/java/fasttrack/logic/commands/exceptions/CommandException.java
@@ -1,4 +1,4 @@
-package seedu.address.logic.commands.exceptions;
+package fasttrack.logic.commands.exceptions;
/**
* Represents an error which occurs during execution of a {@link Command}.
diff --git a/src/main/java/fasttrack/logic/commands/general/CategorySummaryCommand.java b/src/main/java/fasttrack/logic/commands/general/CategorySummaryCommand.java
new file mode 100644
index 00000000000..0b37a27f7e5
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/general/CategorySummaryCommand.java
@@ -0,0 +1,56 @@
+package fasttrack.logic.commands.general;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.model.Model;
+import fasttrack.model.category.Category;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Displays the summary of an expense
+ */
+public class CategorySummaryCommand implements GeneralCommand {
+
+ public static final String COMMAND_WORD = "sumcat";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays summary of category "
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+ public static final String MESSAGE_SUCCESS = "";
+
+ private final Index targetIndex;
+
+ /**
+ * Creates an CategorySummaryCommand to display summary of category at the specified {@code Index}
+ */
+ public CategorySummaryCommand(Index index) {
+ requireNonNull(index);
+ targetIndex = index;
+ }
+
+ @Override
+ public CommandResult execute(Model dataModel) throws CommandException {
+ requireNonNull(dataModel);
+ List lastShownList = dataModel.getFilteredCategoryList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX);
+ }
+
+ Category targetCategory = lastShownList.get(targetIndex.getZeroBased());
+ String toDisplay = targetCategory.getCategoryName() + " summary:\n" + targetCategory.getSummary();
+ return new CommandResult(toDisplay, ScreenType.CATEGORY_SCREEN);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof CategorySummaryCommand // instanceof handles nulls
+ && targetIndex.equals(((CategorySummaryCommand) other).targetIndex)); // state check
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/general/ClearCommand.java b/src/main/java/fasttrack/logic/commands/general/ClearCommand.java
new file mode 100644
index 00000000000..998e536c7ca
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/general/ClearCommand.java
@@ -0,0 +1,25 @@
+package fasttrack.logic.commands.general;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.model.Model;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Format full help instructions for every command for display.
+ */
+public class ClearCommand implements GeneralCommand {
+
+ public static final String COMMAND_WORD = "CLEAR";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes all entries in the FastTrack database.\n"
+ + "Example: " + COMMAND_WORD;
+
+ public static final String MESSAGE_SUCCESS = "Deleted all prior entries.";
+ @Override
+ public CommandResult execute(Model dataModel) {
+ dataModel.clearCategory();
+ dataModel.clearExpense();
+ dataModel.clearRecurringExpenseGenerator();
+ return new CommandResult(MESSAGE_SUCCESS, ScreenType.EXPENSE_SCREEN);
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/general/ExitCommand.java b/src/main/java/fasttrack/logic/commands/general/ExitCommand.java
new file mode 100644
index 00000000000..a30032d3eca
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/general/ExitCommand.java
@@ -0,0 +1,21 @@
+package fasttrack.logic.commands.general;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.model.Model;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Terminates the program.
+ */
+public class ExitCommand implements GeneralCommand {
+
+ public static final String COMMAND_WORD = "exit";
+
+ public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Expense Tracker as requested ...";
+
+ @Override
+ public CommandResult execute(Model dataModel) {
+ return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, ScreenType.EXPENSE_SCREEN);
+ }
+
+}
diff --git a/src/main/java/fasttrack/logic/commands/general/FindCommand.java b/src/main/java/fasttrack/logic/commands/general/FindCommand.java
new file mode 100644
index 00000000000..4d4bda30831
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/general/FindCommand.java
@@ -0,0 +1,45 @@
+package fasttrack.logic.commands.general;
+
+import static java.util.Objects.requireNonNull;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.model.Model;
+import fasttrack.model.expense.ExpenseContainsKeywordsPredicate;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Finds and lists all expenses in the expense tracker whose name contains any of the argument keywords.
+ * Keyword matching is case insensitive.
+ */
+public class FindCommand implements GeneralCommand {
+
+ public static final String COMMAND_WORD = "find";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all expense which names contain any of "
+ + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n"
+ + "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ + "Example: " + COMMAND_WORD + " apple orange cherry";
+
+ private final ExpenseContainsKeywordsPredicate predicate;
+
+ public FindCommand(ExpenseContainsKeywordsPredicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.updateFilteredExpensesList(predicate);
+ return new CommandResult(
+ String.format(Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW,
+ model.getFilteredExpenseList().size()), ScreenType.EXPENSE_SCREEN);
+ }
+
+ @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/fasttrack/logic/commands/general/GeneralCommand.java b/src/main/java/fasttrack/logic/commands/general/GeneralCommand.java
new file mode 100644
index 00000000000..324c71abbfb
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/general/GeneralCommand.java
@@ -0,0 +1,10 @@
+package fasttrack.logic.commands.general;
+
+import fasttrack.logic.commands.Command;
+
+/**
+ * Represents a general command with hidden internal logic and the ability to be
+ * executed.
+ */
+public interface GeneralCommand extends Command {
+}
diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/fasttrack/logic/commands/general/HelpCommand.java
similarity index 60%
rename from src/main/java/seedu/address/logic/commands/HelpCommand.java
rename to src/main/java/fasttrack/logic/commands/general/HelpCommand.java
index bf824f91bd0..f45ed5dc735 100644
--- a/src/main/java/seedu/address/logic/commands/HelpCommand.java
+++ b/src/main/java/fasttrack/logic/commands/general/HelpCommand.java
@@ -1,11 +1,13 @@
-package seedu.address.logic.commands;
+package fasttrack.logic.commands.general;
-import seedu.address.model.Model;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.model.Model;
+import fasttrack.ui.ScreenType;
/**
* Format full help instructions for every command for display.
*/
-public class HelpCommand extends Command {
+public class HelpCommand implements GeneralCommand {
public static final String COMMAND_WORD = "help";
@@ -15,7 +17,7 @@ public class HelpCommand extends Command {
public static final String SHOWING_HELP_MESSAGE = "Opened help window.";
@Override
- public CommandResult execute(Model model) {
- return new CommandResult(SHOWING_HELP_MESSAGE, true, false);
+ public CommandResult execute(Model dataModel) {
+ return new CommandResult(SHOWING_HELP_MESSAGE, true, false, ScreenType.EXPENSE_SCREEN);
}
}
diff --git a/src/main/java/fasttrack/logic/commands/list/ListCategoryCommand.java b/src/main/java/fasttrack/logic/commands/list/ListCategoryCommand.java
new file mode 100644
index 00000000000..a05a8cdb390
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/list/ListCategoryCommand.java
@@ -0,0 +1,26 @@
+package fasttrack.logic.commands.list;
+
+import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES;
+import static java.util.Objects.requireNonNull;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.model.Model;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Lists all categories in the expense tracker to the user.
+ */
+public class ListCategoryCommand implements ListCommand {
+
+ public static final String COMMAND_WORD = "lcat";
+
+ public static final String MESSAGE_SUCCESS = "Listed all categories";
+
+
+ @Override
+ public CommandResult execute(Model dataModel) {
+ requireNonNull(dataModel);
+ dataModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES);
+ return new CommandResult(MESSAGE_SUCCESS, ScreenType.CATEGORY_SCREEN);
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/list/ListCommand.java b/src/main/java/fasttrack/logic/commands/list/ListCommand.java
new file mode 100644
index 00000000000..231f3b3fd93
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/list/ListCommand.java
@@ -0,0 +1,11 @@
+package fasttrack.logic.commands.list;
+
+import fasttrack.logic.commands.Command;
+
+/**
+ * Represents a list command with hidden internal logic and the ability to be
+ * executed.
+ */
+public interface ListCommand extends Command {
+
+}
diff --git a/src/main/java/fasttrack/logic/commands/list/ListExpensesCommand.java b/src/main/java/fasttrack/logic/commands/list/ListExpensesCommand.java
new file mode 100644
index 00000000000..739b45b1b5e
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/list/ListExpensesCommand.java
@@ -0,0 +1,83 @@
+package fasttrack.logic.commands.list;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN;
+import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import fasttrack.commons.core.Messages;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.model.Model;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.ExpenseInCategoryPredicate;
+import fasttrack.model.expense.ExpenseInTimespanPredicate;
+import fasttrack.ui.ScreenType;
+
+/**
+ * List all the expenses in the expense tracker.
+ */
+public class ListExpensesCommand implements ListCommand {
+
+ public static final String COMMAND_WORD = "list";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists expenses based on a filter"
+ + "Parameters: "
+ + PREFIX_CATEGORY + "CATEGORY "
+ + PREFIX_TIMESPAN + "TIMESPAN (Week, Month, Year)\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_CATEGORY + "Food "
+ + PREFIX_TIMESPAN + "week ";
+
+ private final Optional categoryPredicate;
+ private final Optional timespanPredicate;
+
+ /**
+ * Creates a ListCommand to list out {@code Expense} by given filters
+ * @param categoryPredicate Predicate to filter by category
+ * @param timespanPredicate Predicate to filter by recency
+ */
+ public ListExpensesCommand(Optional categoryPredicate,
+ Optional timespanPredicate) {
+ this.categoryPredicate = categoryPredicate;
+ this.timespanPredicate = timespanPredicate;
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ categoryPredicate.ifPresentOrElse(filter -> model.updateCategoryFilter(
+ filter.getCategory()), () -> model.updateCategoryFilter(null));
+ timespanPredicate.ifPresentOrElse(filter -> model.updateTimeSpanFilter(
+ filter.getTimespan()), () -> model.updateTimeSpanFilter(ParserUtil.Timespan.ALL));
+ Predicate combinedPredicate = PREDICATE_SHOW_ALL_EXPENSES;
+ if (categoryPredicate.isPresent()) {
+ combinedPredicate = combinedPredicate.and(categoryPredicate.get());
+ }
+ if (timespanPredicate.isPresent()) {
+ combinedPredicate = combinedPredicate.and(timespanPredicate.get());
+ }
+ model.updateFilteredExpensesList(combinedPredicate);
+ return new CommandResult(
+ String.format(Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW, model.getFilteredExpenseList().size()),
+ ScreenType.EXPENSE_SCREEN);
+
+ }
+
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof ListExpensesCommand // instanceof handles nulls
+ && categoryPredicate.equals(((ListExpensesCommand) other).categoryPredicate)
+ && timespanPredicate.equals(((ListExpensesCommand) other).timespanPredicate)); // state check
+ }
+
+ @Override
+ public String toString() {
+ return COMMAND_WORD + " " + categoryPredicate + " " + timespanPredicate;
+ }
+}
diff --git a/src/main/java/fasttrack/logic/commands/list/ListRecurringExpensesCommand.java b/src/main/java/fasttrack/logic/commands/list/ListRecurringExpensesCommand.java
new file mode 100644
index 00000000000..f58520ffbda
--- /dev/null
+++ b/src/main/java/fasttrack/logic/commands/list/ListRecurringExpensesCommand.java
@@ -0,0 +1,26 @@
+package fasttrack.logic.commands.list;
+
+import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES;
+import static java.util.Objects.requireNonNull;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.model.Model;
+import fasttrack.ui.ScreenType;
+
+/**
+ * Lists all recurring expenses in the FastTrack to the user.
+ */
+public class ListRecurringExpensesCommand implements ListCommand {
+
+ public static final String COMMAND_WORD = "lrec";
+
+ public static final String MESSAGE_SUCCESS = "Listed all recurring expenses";
+
+
+ @Override
+ public CommandResult execute(Model dataModel) {
+ requireNonNull(dataModel);
+ dataModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES);
+ return new CommandResult(MESSAGE_SUCCESS, ScreenType.RECURRING_EXPENSE_SCREEN);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/fasttrack/logic/parser/ArgumentMultimap.java
similarity index 98%
rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java
rename to src/main/java/fasttrack/logic/parser/ArgumentMultimap.java
index 954c8e18f8e..55724582d9b 100644
--- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java
+++ b/src/main/java/fasttrack/logic/parser/ArgumentMultimap.java
@@ -1,4 +1,4 @@
-package seedu.address.logic.parser;
+package fasttrack.logic.parser;
import java.util.ArrayList;
import java.util.HashMap;
diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/fasttrack/logic/parser/ArgumentTokenizer.java
similarity index 99%
rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
rename to src/main/java/fasttrack/logic/parser/ArgumentTokenizer.java
index 5c9aebfa488..0f1a1da1449 100644
--- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
+++ b/src/main/java/fasttrack/logic/parser/ArgumentTokenizer.java
@@ -1,4 +1,4 @@
-package seedu.address.logic.parser;
+package fasttrack.logic.parser;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/src/main/java/fasttrack/logic/parser/CategorySummaryParser.java b/src/main/java/fasttrack/logic/parser/CategorySummaryParser.java
new file mode 100644
index 00000000000..c072aa1907f
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/CategorySummaryParser.java
@@ -0,0 +1,29 @@
+package fasttrack.logic.parser;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.general.CategorySummaryCommand;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new CategorySummaryCommand object
+ */
+public class CategorySummaryParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the CategorySummaryCommand
+ * and returns a CategorySummaryCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public CategorySummaryCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new CategorySummaryCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, CategorySummaryCommand.MESSAGE_USAGE), pe);
+ }
+ }
+
+}
diff --git a/src/main/java/fasttrack/logic/parser/CliSyntax.java b/src/main/java/fasttrack/logic/parser/CliSyntax.java
new file mode 100644
index 00000000000..bc15762d921
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/CliSyntax.java
@@ -0,0 +1,19 @@
+package fasttrack.logic.parser;
+
+
+/**
+ * Contains Command Line Interface (CLI) syntax definitions common to multiple commands
+ */
+public class CliSyntax {
+
+ /* Prefix definitions */
+ public static final Prefix PREFIX_NAME = new Prefix("n/");
+ public static final Prefix PREFIX_CATEGORY = new Prefix("c/");
+ public static final Prefix PREFIX_SUMMARY = new Prefix("s/");
+ public static final Prefix PREFIX_PRICE = new Prefix("p/");
+ public static final Prefix PREFIX_DATE = new Prefix("d/");
+ public static final Prefix PREFIX_TIMESPAN = new Prefix("t/");
+ public static final Prefix PREFIX_START_DATE = new Prefix("sd/");
+ public static final Prefix PREFIX_END_DATE = new Prefix("ed/");
+
+}
diff --git a/src/main/java/fasttrack/logic/parser/ExpenseTrackerParser.java b/src/main/java/fasttrack/logic/parser/ExpenseTrackerParser.java
new file mode 100644
index 00000000000..7f892896cb3
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/ExpenseTrackerParser.java
@@ -0,0 +1,126 @@
+package fasttrack.logic.parser;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import fasttrack.logic.commands.Command;
+import fasttrack.logic.commands.SetBudgetCommand;
+import fasttrack.logic.commands.add.AddCategoryCommand;
+import fasttrack.logic.commands.add.AddExpenseCommand;
+import fasttrack.logic.commands.add.AddRecurringExpenseCommand;
+import fasttrack.logic.commands.delete.DeleteCategoryCommand;
+import fasttrack.logic.commands.delete.DeleteExpenseCommand;
+import fasttrack.logic.commands.delete.DeleteRecurringExpenseCommand;
+import fasttrack.logic.commands.edit.EditCategoryCommand;
+import fasttrack.logic.commands.edit.EditExpenseCommand;
+import fasttrack.logic.commands.edit.EditRecurringExpenseManagerCommand;
+import fasttrack.logic.commands.general.CategorySummaryCommand;
+import fasttrack.logic.commands.general.ClearCommand;
+import fasttrack.logic.commands.general.ExitCommand;
+import fasttrack.logic.commands.general.FindCommand;
+import fasttrack.logic.commands.general.HelpCommand;
+import fasttrack.logic.commands.list.ListCategoryCommand;
+import fasttrack.logic.commands.list.ListExpensesCommand;
+import fasttrack.logic.commands.list.ListRecurringExpensesCommand;
+import fasttrack.logic.parser.add.AddCategoryCommandParser;
+import fasttrack.logic.parser.add.AddExpenseCommandParser;
+import fasttrack.logic.parser.add.AddRecurringExpenseCommandParser;
+import fasttrack.logic.parser.delete.DeleteCategoryCommandParser;
+import fasttrack.logic.parser.delete.DeleteExpenseCommandParser;
+import fasttrack.logic.parser.delete.DeleteRecurringExpenseCommandParser;
+import fasttrack.logic.parser.edit.EditCategoryCommandParser;
+import fasttrack.logic.parser.edit.EditExpenseCommandParser;
+import fasttrack.logic.parser.edit.EditRecurringExpenseManagerCommandParser;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses user input.
+ */
+public class ExpenseTrackerParser {
+
+ /**
+ * 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:
+ return new AddExpenseCommandParser().parse(arguments);
+
+ case DeleteExpenseCommand.COMMAND_WORD:
+ return new DeleteExpenseCommandParser().parse(arguments);
+
+ case ListExpensesCommand.COMMAND_WORD:
+ return new ListCommandParser().parse(arguments);
+
+ case ListCategoryCommand.COMMAND_WORD:
+ return new ListCategoryCommand();
+
+ case ExitCommand.COMMAND_WORD:
+ return new ExitCommand();
+
+ case FindCommand.COMMAND_WORD:
+ return new FindCommandParser().parse(arguments);
+
+ case HelpCommand.COMMAND_WORD:
+ return new HelpCommand();
+
+ case AddCategoryCommand.COMMAND_WORD:
+ return new AddCategoryCommandParser().parse(arguments);
+
+ case DeleteCategoryCommand.COMMAND_WORD:
+ return new DeleteCategoryCommandParser().parse(arguments);
+
+ case EditCategoryCommand.COMMAND_WORD:
+ return new EditCategoryCommandParser().parse(arguments);
+
+ case EditExpenseCommand.COMMAND_WORD:
+ return new EditExpenseCommandParser().parse(arguments);
+
+ case SetBudgetCommand.COMMAND_WORD:
+ return new SetBudgetParser().parse(arguments);
+
+ case CategorySummaryCommand.COMMAND_WORD:
+ return new CategorySummaryParser().parse(arguments);
+
+ case ClearCommand.COMMAND_WORD:
+ return new ClearCommand();
+
+ case EditRecurringExpenseManagerCommand.COMMAND_WORD:
+ return new EditRecurringExpenseManagerCommandParser().parse(arguments);
+
+ case AddRecurringExpenseCommand.COMMAND_WORD:
+ return new AddRecurringExpenseCommandParser().parse(arguments);
+
+ case DeleteRecurringExpenseCommand.COMMAND_WORD:
+ return new DeleteRecurringExpenseCommandParser().parse(arguments);
+
+ case ListRecurringExpensesCommand.COMMAND_WORD:
+ return new ListRecurringExpensesCommand();
+
+ default:
+ throw new ParseException(MESSAGE_UNKNOWN_COMMAND);
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/fasttrack/logic/parser/FindCommandParser.java
similarity index 66%
rename from src/main/java/seedu/address/logic/parser/FindCommandParser.java
rename to src/main/java/fasttrack/logic/parser/FindCommandParser.java
index 4fb71f23103..32e63459b2c 100644
--- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java
+++ b/src/main/java/fasttrack/logic/parser/FindCommandParser.java
@@ -1,12 +1,12 @@
-package seedu.address.logic.parser;
+package fasttrack.logic.parser;
-import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import java.util.Arrays;
-import seedu.address.logic.commands.FindCommand;
-import seedu.address.logic.parser.exceptions.ParseException;
-import seedu.address.model.person.NameContainsKeywordsPredicate;
+import fasttrack.logic.commands.general.FindCommand;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.expense.ExpenseContainsKeywordsPredicate;
/**
* Parses input arguments and creates a new FindCommand object
@@ -27,7 +27,7 @@ public FindCommand parse(String args) throws ParseException {
String[] nameKeywords = trimmedArgs.split("\\s+");
- return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords)));
+ return new FindCommand(new ExpenseContainsKeywordsPredicate(Arrays.asList(nameKeywords)));
}
}
diff --git a/src/main/java/fasttrack/logic/parser/ListCommandParser.java b/src/main/java/fasttrack/logic/parser/ListCommandParser.java
new file mode 100644
index 00000000000..401cd905f73
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/ListCommandParser.java
@@ -0,0 +1,56 @@
+package fasttrack.logic.parser;
+
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN;
+
+import java.util.Optional;
+
+import fasttrack.logic.commands.list.ListExpensesCommand;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.expense.ExpenseInCategoryPredicate;
+import fasttrack.model.expense.ExpenseInTimespanPredicate;
+
+/**
+ * Parses input arguments and creates a new AddCommand object
+ */
+public class ListCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddCommand
+ * and returns an AddCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public ListExpensesCommand parse(String args) throws ParseException {
+
+ if (args.equals("")) {
+ return new ListExpensesCommand(Optional.empty(), Optional.empty());
+ }
+
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_TIMESPAN);
+
+ if (!argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListExpensesCommand.MESSAGE_USAGE));
+ }
+
+ Optional categoryStringArg = argMultimap.getValue(PREFIX_CATEGORY);
+ Optional timespanStringArg = argMultimap.getValue(PREFIX_TIMESPAN);
+
+ ExpenseInCategoryPredicate categoryPredicate = null;
+ ExpenseInTimespanPredicate timespanPredicate = null;
+
+ if (categoryStringArg.isPresent()) {
+ categoryPredicate = new ExpenseInCategoryPredicate(ParserUtil.parseCategory(categoryStringArg.get()));
+ }
+ if (timespanStringArg.isPresent()) {
+ timespanPredicate = new ExpenseInTimespanPredicate(ParserUtil.parseTimespan(timespanStringArg.get()));
+ }
+
+
+ return new ListExpensesCommand(
+ Optional.ofNullable(categoryPredicate),
+ Optional.ofNullable(timespanPredicate));
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/fasttrack/logic/parser/Parser.java
similarity index 72%
rename from src/main/java/seedu/address/logic/parser/Parser.java
rename to src/main/java/fasttrack/logic/parser/Parser.java
index d6551ad8e3f..997322e0381 100644
--- a/src/main/java/seedu/address/logic/parser/Parser.java
+++ b/src/main/java/fasttrack/logic/parser/Parser.java
@@ -1,7 +1,7 @@
-package seedu.address.logic.parser;
+package fasttrack.logic.parser;
-import seedu.address.logic.commands.Command;
-import seedu.address.logic.parser.exceptions.ParseException;
+import fasttrack.logic.commands.Command;
+import fasttrack.logic.parser.exceptions.ParseException;
/**
* Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}.
diff --git a/src/main/java/fasttrack/logic/parser/ParserUtil.java b/src/main/java/fasttrack/logic/parser/ParserUtil.java
new file mode 100644
index 00000000000..8f82a910a7a
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/ParserUtil.java
@@ -0,0 +1,199 @@
+package fasttrack.logic.parser;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_DATE_FORMAT;
+import static java.util.Objects.requireNonNull;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.commons.util.StringUtil;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.category.Category;
+import fasttrack.model.category.MiscellaneousCategory;
+import fasttrack.model.category.UserDefinedCategory;
+import fasttrack.model.expense.Price;
+import fasttrack.model.expense.RecurringExpenseType;
+import fasttrack.model.util.CommandUtility;
+
+/**
+ * Contains utility methods used for parsing strings in the various *Parser classes.
+ */
+public class ParserUtil {
+
+ /**
+ * Enumerates possibilities for timespans indicated: week, month, year.
+ */
+ public enum Timespan {
+ WEEK("Week"),
+ MONTH("Month"),
+ YEAR("Year"),
+ ALL("All");
+
+ private final String stringRep;
+ Timespan(String stringRep) {
+ this.stringRep = stringRep;
+ }
+ // Return the string representation of the given timeSpan
+ @Override
+ public String toString() {
+ return stringRep;
+ }
+ }
+
+
+ public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid.";
+
+ /**
+ * Parses {@code oneBasedIndex} into an {@code Index} and returns it.
+ * Leading and trailing whitespaces will be
+ * trimmed.
+ * @throws ParseException if the specified index is invalid (not non-zero unsigned integer).
+ */
+ public static Index parseIndex(String oneBasedIndex) throws ParseException {
+ String trimmedIndex = oneBasedIndex.trim();
+ if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) {
+ throw new ParseException(MESSAGE_INVALID_INDEX);
+ }
+ return Index.fromOneBased(Integer.parseInt(trimmedIndex));
+ }
+
+ /**
+ * Parses a {@code String expenseName} into a String.
+ * Leading and trailing whitespaces will be trimmed.
+ * @throws ParseException if the given {@code expenseName} is invalid.
+ */
+ public static String parseExpenseName(String expenseName) throws ParseException {
+ requireNonNull(expenseName);
+ String trimmedName = expenseName.trim();
+ if (trimmedName.isEmpty()) {
+ throw new ParseException("The expense name should not be empty!");
+ }
+ return trimmedName;
+ }
+
+ /**
+ * Parses {@code price} into an {@code Price} and returns it.
+ * Leading and trailing whitespaces will be trimmed.
+ * @throws ParseException if the specified price is invalid (not non-negative and numeric).
+ */
+ public static Price parsePrice(String price) throws ParseException {
+ requireNonNull(price);
+ String trimmedPrice = price.trim();
+ try {
+ if (Price.isValidPrice(trimmedPrice)) {
+ return new Price(trimmedPrice);
+ } else {
+ throw new ParseException(Price.MESSAGE_CONSTRAINTS);
+ }
+ } catch (NumberFormatException e) {
+ throw new ParseException(Price.MESSAGE_CONSTRAINTS);
+ }
+ }
+
+ /**
+ * Parses {@code categoryName} and creates a {@code Category} instance and returns it.
+ * Leading and trailing whitespaces will be
+ * trimmed.
+ * @throws ParseException if the specified categoryName does not exist
+ */
+ public static Category parseCategory(String categoryName) throws ParseException {
+ String trimmedCategoryName = categoryName.trim();
+ if (trimmedCategoryName.isEmpty() || !Category.isValidCategoryName(trimmedCategoryName)) {
+ throw new ParseException(Category.MESSAGE_CONSTRAINTS);
+ }
+ if (trimmedCategoryName.equals("miscellaneous")) {
+ return new MiscellaneousCategory();
+ }
+ return new UserDefinedCategory(trimmedCategoryName, "");
+ }
+
+ /**
+ * Parses a {@code String category} into a {@code UserDefinedCategory}.
+ * Leading and trailing whitespaces will be trimmed.
+ * @throws ParseException if the given {@code category} is invalid.
+ */
+ public static UserDefinedCategory parseCategory(String category, String summary) throws ParseException {
+ requireNonNull(category);
+ String trimmedCategory = category.trim();
+ if (!Category.isValidCategoryName(trimmedCategory)) {
+ throw new ParseException(Category.MESSAGE_CONSTRAINTS);
+ }
+ return new UserDefinedCategory(trimmedCategory, summary);
+ }
+
+ /**
+ * Parses {@code dateString} into a {@code LocalDate} instance and returns it.
+ * @throws ParseException if the date could not be parsed
+ */
+ public static LocalDate parseDate(String dateString) throws ParseException {
+ String trimmedDate = dateString.trim();
+ LocalDate parsedDate;
+ try {
+ parsedDate = CommandUtility.parseDateFromUserInput(trimmedDate);
+ } catch (IllegalArgumentException e) {
+ throw new ParseException(MESSAGE_INVALID_DATE_FORMAT);
+ }
+ return parsedDate;
+ }
+ /**
+ * Parses {@code String timespan} into a {@code LocalDate}.
+ */
+ public static Timespan parseTimespan(String timespan) throws ParseException {
+ assert timespan != null : "input should not be null";
+ requireNonNull(timespan);
+ String trimmedTimespan = timespan.trim();
+ if (trimmedTimespan.equalsIgnoreCase("week") || trimmedTimespan.equalsIgnoreCase("w")) {
+ return Timespan.WEEK;
+ }
+ if (trimmedTimespan.equalsIgnoreCase("month") || trimmedTimespan.equalsIgnoreCase("m")) {
+ return Timespan.MONTH;
+ }
+ if (trimmedTimespan.equalsIgnoreCase("year") || trimmedTimespan.equalsIgnoreCase("y")) {
+ return Timespan.YEAR;
+ }
+ throw new ParseException("Not a valid date format (week, month, year)");
+ }
+
+ /**
+ * Parses {@code String timespan} into a {@code RecurringExpenseType}.
+ */
+ public static RecurringExpenseType parseTimeSpanRecurringExpense(String timespan) throws ParseException {
+ assert timespan != null : "input should not be null";
+ requireNonNull(timespan);
+ String trimmedTimespan = timespan.trim();
+ if (trimmedTimespan.equalsIgnoreCase("day") || trimmedTimespan.equalsIgnoreCase("d")) {
+ return RecurringExpenseType.DAILY;
+ }
+ if (trimmedTimespan.equalsIgnoreCase("week") || trimmedTimespan.equalsIgnoreCase("w")) {
+ return RecurringExpenseType.WEEKLY;
+ }
+ if (trimmedTimespan.equalsIgnoreCase("month") || trimmedTimespan.equalsIgnoreCase("m")) {
+ return RecurringExpenseType.MONTHLY;
+ }
+ if (trimmedTimespan.equalsIgnoreCase("year") || trimmedTimespan.equalsIgnoreCase("y")) {
+ return RecurringExpenseType.YEARLY;
+ }
+ throw new ParseException("Not a valid date format (day, week, month, year)");
+ }
+
+ /**
+ * Get earliest {@code LocalDate} within the given {@code Timespan}.
+ * @param t {@code Timespan} of week, month or year
+ * @return LocalDate of the earliest date in this timespan
+ */
+ public static LocalDate getDateByTimespan(Timespan t) {
+ switch (t) {
+ case WEEK:
+ return LocalDate.now().with(DayOfWeek.MONDAY);
+ case MONTH:
+ return LocalDate.now().withDayOfMonth(1);
+ case YEAR:
+ return LocalDate.now().withDayOfYear(1);
+ default:
+ break;
+ }
+ assert false : "Line should not be reached";
+ return null;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/fasttrack/logic/parser/Prefix.java
similarity index 95%
rename from src/main/java/seedu/address/logic/parser/Prefix.java
rename to src/main/java/fasttrack/logic/parser/Prefix.java
index c859d5fa5db..d5ed018f6b0 100644
--- a/src/main/java/seedu/address/logic/parser/Prefix.java
+++ b/src/main/java/fasttrack/logic/parser/Prefix.java
@@ -1,4 +1,4 @@
-package seedu.address.logic.parser;
+package fasttrack.logic.parser;
/**
* A prefix that marks the beginning of an argument in an arguments string.
diff --git a/src/main/java/fasttrack/logic/parser/SetBudgetParser.java b/src/main/java/fasttrack/logic/parser/SetBudgetParser.java
new file mode 100644
index 00000000000..4bcbff4eb15
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/SetBudgetParser.java
@@ -0,0 +1,46 @@
+package fasttrack.logic.parser;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+
+import java.util.stream.Stream;
+
+import fasttrack.logic.commands.SetBudgetCommand;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.Budget;
+import fasttrack.model.expense.Price;
+
+/**
+ * Parses input arguments and creates a new AddExpenseCommand object
+ */
+public class SetBudgetParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddExpenseCommand
+ * and returns an AddExpenseCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public SetBudgetCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_PRICE);
+
+ if (!arePrefixesPresent(argMultimap, PREFIX_PRICE)
+ || !argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetBudgetCommand.MESSAGE_USAGE));
+ }
+
+ Price amount = ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get());
+ Budget budget = new Budget(amount.getPriceAsDouble());
+
+ return new SetBudgetCommand(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/fasttrack/logic/parser/add/AddCategoryCommandParser.java b/src/main/java/fasttrack/logic/parser/add/AddCategoryCommandParser.java
new file mode 100644
index 00000000000..29c11dfe062
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/add/AddCategoryCommandParser.java
@@ -0,0 +1,54 @@
+package fasttrack.logic.parser.add;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY;
+
+import java.util.stream.Stream;
+
+import fasttrack.logic.commands.add.AddCategoryCommand;
+import fasttrack.logic.parser.ArgumentMultimap;
+import fasttrack.logic.parser.ArgumentTokenizer;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.Prefix;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.category.Category;
+
+/**
+ * Parses input arguments and creates a new AddCategoryCommand object
+ */
+public class AddCategoryCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddCommand
+ * and returns an AddCategoryCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public AddCategoryCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_SUMMARY);
+
+ if (!arePrefixesPresent(argMultimap, PREFIX_CATEGORY)
+ || !argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCategoryCommand.MESSAGE_USAGE));
+ }
+ String summary = "";
+ if (arePrefixesPresent(argMultimap, PREFIX_SUMMARY)) {
+ summary = argMultimap.getValue(PREFIX_SUMMARY).get();
+ }
+
+ Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get(), summary);
+
+ return new AddCategoryCommand(category);
+ }
+
+ /**
+ * 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/fasttrack/logic/parser/add/AddExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/add/AddExpenseCommandParser.java
new file mode 100644
index 00000000000..be2cb234a11
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/add/AddExpenseCommandParser.java
@@ -0,0 +1,66 @@
+package fasttrack.logic.parser.add;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import fasttrack.logic.commands.add.AddExpenseCommand;
+import fasttrack.logic.parser.ArgumentMultimap;
+import fasttrack.logic.parser.ArgumentTokenizer;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.Prefix;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.Price;
+
+/**
+ * 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 AddExpenseCommand 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_CATEGORY, PREFIX_PRICE, PREFIX_DATE);
+
+ if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_CATEGORY, PREFIX_PRICE)
+ || !argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExpenseCommand.MESSAGE_USAGE));
+ }
+
+ String name = ParserUtil.parseExpenseName(argMultimap.getValue(PREFIX_NAME).get());
+ Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get());
+ Price amount = ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get());
+
+ Optional dateStringArg = argMultimap.getValue(PREFIX_DATE);
+
+ LocalDate date = LocalDate.now();
+ if (dateStringArg.isPresent()) {
+ date = ParserUtil.parseDate(dateStringArg.get());
+ }
+
+ Expense expenseToAdd = new Expense(name, amount, date, category);
+ return new AddExpenseCommand(expenseToAdd);
+ }
+
+ /**
+ * 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/fasttrack/logic/parser/add/AddRecurringExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParser.java
new file mode 100644
index 00000000000..bb42f842bf2
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParser.java
@@ -0,0 +1,78 @@
+package fasttrack.logic.parser.add;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_START_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN;
+
+import java.time.LocalDate;
+import java.util.stream.Stream;
+
+import fasttrack.logic.commands.add.AddRecurringExpenseCommand;
+import fasttrack.logic.parser.ArgumentMultimap;
+import fasttrack.logic.parser.ArgumentTokenizer;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.Prefix;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Price;
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.model.expense.RecurringExpenseType;
+
+/**
+ * Parses input arguments and creates a new AddCategoryCommand object
+ */
+public class AddRecurringExpenseCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddCommand
+ * and returns an AddCategoryCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public AddRecurringExpenseCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_CATEGORY, PREFIX_PRICE , PREFIX_START_DATE,
+ PREFIX_END_DATE, PREFIX_TIMESPAN);
+
+ if (!arePrefixesPresent(argMultimap, PREFIX_CATEGORY, PREFIX_START_DATE, PREFIX_PRICE, PREFIX_TIMESPAN,
+ PREFIX_NAME)
+ || !argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ AddRecurringExpenseCommand.MESSAGE_USAGE));
+ }
+
+ Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get());
+ Price price = ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get());
+ String name = ParserUtil.parseExpenseName(argMultimap.getValue(PREFIX_NAME).get());
+ LocalDate startDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_START_DATE).get());
+ RecurringExpenseType timespan = ParserUtil.parseTimeSpanRecurringExpense(
+ argMultimap.getValue(PREFIX_TIMESPAN).get());
+ LocalDate endDate = null;
+ if (arePrefixesPresent(argMultimap, PREFIX_END_DATE)) {
+ endDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_END_DATE).get());
+ if (endDate.isBefore(startDate)) {
+ throw new ParseException("End date provided is earlier than start date.");
+ }
+ RecurringExpenseManager toAdd = new RecurringExpenseManager(name, price,
+ category, startDate, endDate, timespan);
+ return new AddRecurringExpenseCommand(toAdd);
+ }
+
+ RecurringExpenseManager toAdd = new RecurringExpenseManager(name, price,
+ category, startDate, timespan);
+ return new AddRecurringExpenseCommand(toAdd);
+ }
+
+ /**
+ * 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/fasttrack/logic/parser/delete/DeleteCategoryCommandParser.java b/src/main/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParser.java
new file mode 100644
index 00000000000..c8b1337702f
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParser.java
@@ -0,0 +1,31 @@
+package fasttrack.logic.parser.delete;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.delete.DeleteCategoryCommand;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteCommand object
+ */
+public class DeleteCategoryCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteCommand
+ * and returns a DeleteCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public DeleteCategoryCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new DeleteCategoryCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCategoryCommand.MESSAGE_USAGE), pe);
+ }
+ }
+
+}
diff --git a/src/main/java/fasttrack/logic/parser/delete/DeleteExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/delete/DeleteExpenseCommandParser.java
new file mode 100644
index 00000000000..82aaaefadb8
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/delete/DeleteExpenseCommandParser.java
@@ -0,0 +1,31 @@
+package fasttrack.logic.parser.delete;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.delete.DeleteExpenseCommand;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteExpenseCommandParser object
+ */
+public class DeleteExpenseCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteExpenseCommandParser
+ * and returns a DeleteExpenseCommandParser 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/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParser.java
new file mode 100644
index 00000000000..f80afd3b570
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParser.java
@@ -0,0 +1,31 @@
+package fasttrack.logic.parser.delete;
+
+import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.delete.DeleteRecurringExpenseCommand;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteExpenseCommandParser object
+ */
+public class DeleteRecurringExpenseCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteExpenseCommandParser
+ * and returns a DeleteExpenseCommandParser object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public DeleteRecurringExpenseCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new DeleteRecurringExpenseCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteRecurringExpenseCommand.MESSAGE_USAGE), pe);
+ }
+ }
+
+}
diff --git a/src/main/java/fasttrack/logic/parser/edit/EditCategoryCommandParser.java b/src/main/java/fasttrack/logic/parser/edit/EditCategoryCommandParser.java
new file mode 100644
index 00000000000..e41654c4fec
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/edit/EditCategoryCommandParser.java
@@ -0,0 +1,61 @@
+package fasttrack.logic.parser.edit;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.edit.EditCategoryCommand;
+import fasttrack.logic.parser.ArgumentMultimap;
+import fasttrack.logic.parser.ArgumentTokenizer;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.Prefix;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new EditCategory object
+ */
+public class EditCategoryCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the EditCategory
+ * @param args Arguments provided by user in String form.
+ * @return an instance of EditCategory with the necessary arguments extracted from user's arguments.
+ * @throws ParseException if the user input does not conform to required format.
+ */
+ public EditCategoryCommand parse(String args) throws ParseException {
+ //First check if the given index is valid.
+ ArgumentMultimap argMultiMap =
+ ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_SUMMARY);
+ Index index = ParserUtil.parseIndex(argMultiMap.getPreamble());
+
+ //Get the new category name & summary if applicable
+ if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY) && isPrefixPresent(argMultiMap, PREFIX_SUMMARY)) {
+ String newSummary = argMultiMap.getValue(PREFIX_SUMMARY).get();
+ String newCategoryName = argMultiMap.getValue(PREFIX_CATEGORY).get();
+ return new EditCategoryCommand(index, newCategoryName, newSummary);
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY)) {
+ String newCategoryName = argMultiMap.getValue(PREFIX_CATEGORY).get();
+ return new EditCategoryCommand(index, newCategoryName, null);
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_SUMMARY)) {
+ String newSummary = argMultiMap.getValue(PREFIX_SUMMARY).get();
+ return new EditCategoryCommand(index, null, newSummary);
+ }
+
+ return new EditCategoryCommand(index, null, null);
+ }
+
+ /**
+ * Returns true if the given prefix does not contain {@code Optional} values in the given {@code ArgumentMultimap}
+ * @param argMultiMap The argument multimap to check for.
+ * @param toCheck The prefix to check for.
+ * @return Boolean that indicates whether the given prefix is present or not.
+ */
+ private static boolean isPrefixPresent(ArgumentMultimap argMultiMap, Prefix toCheck) {
+ return argMultiMap.getValue(toCheck).isPresent();
+ }
+}
diff --git a/src/main/java/fasttrack/logic/parser/edit/EditExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/edit/EditExpenseCommandParser.java
new file mode 100644
index 00000000000..94daf37f691
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/edit/EditExpenseCommandParser.java
@@ -0,0 +1,88 @@
+package fasttrack.logic.parser.edit;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+
+import java.time.LocalDate;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.edit.EditExpenseCommand;
+import fasttrack.logic.parser.ArgumentMultimap;
+import fasttrack.logic.parser.ArgumentTokenizer;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.Prefix;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * 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 EditCategory
+ * @param args Arguments provided by user in String form.
+ * @return EditExpenseCommand that will carry out the user's arguments to edit the expense specified.
+ * @throws ParseException if the user input does not conform to required format.
+ */
+ public EditExpenseCommand parse(String args) throws ParseException {
+ //First check if the given index is valid.
+ ArgumentMultimap argMultiMap =
+ ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_DATE, PREFIX_NAME, PREFIX_PRICE);
+ Index index = ParserUtil.parseIndex(argMultiMap.getPreamble());
+ String newExpenseName;
+ Double newPrice;
+ LocalDate newDate;
+ String newCategory;
+
+ //Get the new category name if applicable
+ if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY)) {
+ newCategory = argMultiMap.getValue(PREFIX_CATEGORY).get();
+ } else {
+ newCategory = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_NAME)) {
+ newExpenseName = argMultiMap.getValue(PREFIX_NAME).get();
+ } else {
+ newExpenseName = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_PRICE)) {
+ String inputPriceInString = argMultiMap.getValue(PREFIX_PRICE).get();
+ try {
+ newPrice = Double.parseDouble(inputPriceInString);
+ } catch (NumberFormatException nfe) {
+ throw new ParseException("Invalid price!", nfe);
+ }
+ } else {
+ newPrice = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_DATE)) {
+ String inputDateInString = argMultiMap.getValue(PREFIX_DATE).get();
+ try {
+ newDate = ParserUtil.parseDate(inputDateInString);
+ } catch (ParseException pe) {
+ throw new ParseException("Invalid date format! Please use DD/MM/YY format!", pe);
+ }
+ } else {
+ newDate = null;
+ }
+
+ return new EditExpenseCommand(index, newExpenseName, newPrice, newDate, newCategory);
+ }
+
+ /**
+ * Returns true if the given prefix does not contain {@code Optional} values in the given {@code ArgumentMultimap}
+ * @param argMultiMap The argument multimap to check for.
+ * @param toCheck The prefix to check for.
+ * @return Boolean that indicates whether the given prefix is present or not.
+ */
+ private static boolean isPrefixPresent(ArgumentMultimap argMultiMap, Prefix toCheck) {
+ return argMultiMap.getValue(toCheck).isPresent();
+ }
+
+}
diff --git a/src/main/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParser.java b/src/main/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParser.java
new file mode 100644
index 00000000000..60ebb8eced7
--- /dev/null
+++ b/src/main/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParser.java
@@ -0,0 +1,99 @@
+package fasttrack.logic.parser.edit;
+
+import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE;
+import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN;
+
+import java.time.LocalDate;
+
+import fasttrack.commons.core.index.Index;
+import fasttrack.logic.commands.edit.EditRecurringExpenseManagerCommand;
+import fasttrack.logic.parser.ArgumentMultimap;
+import fasttrack.logic.parser.ArgumentTokenizer;
+import fasttrack.logic.parser.Parser;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.logic.parser.Prefix;
+import fasttrack.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new EditRecurringExpenseManagerCommand object
+ */
+public class EditRecurringExpenseManagerCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the EditCategory
+ * @param args Arguments provided by user in String form.
+ * @return EditRecurringExpenseManagerCommand that will carry out the user's arguments to edit the recurring expense
+ * specified.
+ * @throws ParseException if the user input does not conform to required format.
+ */
+ public EditRecurringExpenseManagerCommand parse(String args) throws ParseException {
+ //First check if the given index is valid.
+ ArgumentMultimap argMultiMap =
+ ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_END_DATE, PREFIX_NAME, PREFIX_PRICE,
+ PREFIX_TIMESPAN);
+ Index index = ParserUtil.parseIndex(argMultiMap.getPreamble());
+ String newExpenseName;
+ Double newPrice;
+ LocalDate newEndDate;
+ String newCategory;
+ String newFrequency;
+
+ if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY)) {
+ newCategory = argMultiMap.getValue(PREFIX_CATEGORY).get();
+ } else {
+ newCategory = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_NAME)) {
+ newExpenseName = argMultiMap.getValue(PREFIX_NAME).get();
+ } else {
+ newExpenseName = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_PRICE)) {
+ String inputPriceInString = argMultiMap.getValue(PREFIX_PRICE).get();
+ try {
+ newPrice = Double.parseDouble(inputPriceInString);
+ } catch (NumberFormatException nfe) {
+ throw new ParseException("Invalid price!", nfe);
+ }
+ } else {
+ newPrice = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_END_DATE)) {
+ String inputDateInString = argMultiMap.getValue(PREFIX_END_DATE).get();
+ try {
+ newEndDate = ParserUtil.parseDate(inputDateInString);
+ } catch (ParseException pe) {
+ throw new ParseException("Invalid date format! Please use DD/MM/YY format!", pe);
+ }
+ } else {
+ newEndDate = null;
+ }
+
+ if (isPrefixPresent(argMultiMap, PREFIX_TIMESPAN)) {
+ String inputFrequencyInString = argMultiMap.getValue(PREFIX_TIMESPAN).get();
+ newFrequency = inputFrequencyInString.toUpperCase();
+ } else {
+ newFrequency = null;
+ }
+
+ return new EditRecurringExpenseManagerCommand(index, newExpenseName, newPrice, newCategory, newFrequency,
+ newEndDate);
+
+ }
+
+ /**
+ * Returns true if the given prefix does not contain {@code Optional} values in the given {@code ArgumentMultimap}
+ * @param argMultiMap The argument multimap to check for.
+ * @param toCheck The prefix to check for.
+ * @return Boolean that indicates whether the given prefix is present or not.
+ */
+ private static boolean isPrefixPresent(ArgumentMultimap argMultiMap, Prefix toCheck) {
+ return argMultiMap.getValue(toCheck).isPresent();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/fasttrack/logic/parser/exceptions/ParseException.java
similarity index 73%
rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java
rename to src/main/java/fasttrack/logic/parser/exceptions/ParseException.java
index 158a1a54c1c..ecc7cccf02c 100644
--- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java
+++ b/src/main/java/fasttrack/logic/parser/exceptions/ParseException.java
@@ -1,6 +1,6 @@
-package seedu.address.logic.parser.exceptions;
+package fasttrack.logic.parser.exceptions;
-import seedu.address.commons.exceptions.IllegalValueException;
+import fasttrack.commons.exceptions.IllegalValueException;
/**
* Represents a parse error encountered by a parser.
diff --git a/src/main/java/fasttrack/model/AnalyticModel.java b/src/main/java/fasttrack/model/AnalyticModel.java
new file mode 100644
index 00000000000..9513b290d73
--- /dev/null
+++ b/src/main/java/fasttrack/model/AnalyticModel.java
@@ -0,0 +1,102 @@
+package fasttrack.model;
+
+import fasttrack.model.util.AnalyticsType;
+import javafx.beans.property.DoubleProperty;
+
+/**
+ * The AnalyticModelManager class represents the in-memory model of user analytics from the expense tracker.
+ * It provides various methods for calculating and retrieving statistics related to user expenses.
+ */
+public interface AnalyticModel {
+ /**
+ * Returns a DoubleProperty representing analytics data for a given AnalyticsType.
+ * @param type an AnalyticsType enum that specifies which type of analytics data to return
+ * @return a DoubleProperty representing the requested analytics data
+ * @throws IllegalArgumentException if the given analytics type is not supported
+ */
+ DoubleProperty getAnalyticsData(AnalyticsType type) throws IllegalArgumentException;
+
+ /**
+ * Calculates the total amount spent in the current month based
+ * on the filtered expenses list and updates the value of monthlySpent property
+ * @return DoubleProperty representing the monthly spent amount
+ */
+ DoubleProperty getMonthlySpent();
+
+ /**
+ * Calculates remaining budget for the current month
+ * and updates the value of monthlyRemaining property
+ * @return DoubleProperty representing the remaining budget for the month
+ */
+ DoubleProperty getMonthlyRemaining();
+
+ /**
+ * Calculates total amount spent during the current week
+ * based on the filtered expenses list and updates the value of weeklySpent property
+ * @return DoubleProperty representing the total amount spent during the current week
+ */
+ DoubleProperty getWeeklySpent();
+
+ /**
+ * Calculates remaining budget for the current week
+ * and updates the value of weeklyRemaining property
+ * @return DoubleProperty representing the remaining budget for the week
+ */
+ DoubleProperty getWeeklyRemaining();
+
+ /**
+ * Calculates percentage change in spending from the previous week
+ * to the current week and updates the value of weeklyChange property
+ * @return DoubleProperty representing percentage change
+ */
+ DoubleProperty getWeeklyChange();
+
+ /**
+ * Calculates percentage change in spending from the previous month
+ * to the current month and updates the value of monthlyChange property
+ * @return DoubleProperty representing percentage change in spending
+ */
+ DoubleProperty getMonthlyChange();
+
+ /**
+ * Calculates the total amount of money spent on all expenses
+ * @return a DoubleProperty representing the total amount spent
+ */
+ DoubleProperty getTotalSpent();
+
+ /**
+ * Calculates and returns the percentage of the monthly budget that has been spent so far
+ * The percentage is capped at 100% if it exceeds 100
+ * @return a DoubleProperty representing the percentage of budget spent
+ */
+ DoubleProperty getBudgetPercentage();
+
+ /**
+ * Updates the currently referenced monthly budget in the GUI
+ * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed.
+ * @param newBudget the new Budget object value which changed
+ */
+ void updateMonthlyBudgetProperty(Budget newBudget);
+
+ /**
+ * Updates the currently referenced weekly budget in the GUI
+ * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed.
+ * @param newBudget the new Budget object value which changed
+ */
+ void updateWeeklyBudgetProperty(Budget newBudget);
+
+ /**
+ * Gets the value of the users current monthly budget
+ */
+ double getBudget();
+
+ /**
+ * Gets the Property of the users current monthly budget
+ */
+ DoubleProperty getMonthlyBudgetProperty();
+
+ /**
+ * A convenience method to re-calculate and update all statistics
+ */
+ void updateAllStatistics();
+}
diff --git a/src/main/java/fasttrack/model/AnalyticModelManager.java b/src/main/java/fasttrack/model/AnalyticModelManager.java
new file mode 100644
index 00000000000..00dade97865
--- /dev/null
+++ b/src/main/java/fasttrack/model/AnalyticModelManager.java
@@ -0,0 +1,353 @@
+package fasttrack.model;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.temporal.TemporalAdjusters;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.util.AnalyticsType;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+/**
+ * The AnalyticModelManager class represents the in-memory model of user analytics from the expense tracker.
+ * It provides various methods for calculating and retrieving statistics related to user expenses.
+ */
+public class AnalyticModelManager implements AnalyticModel {
+ private final ObservableList allExpenses;
+ private final ObservableList allCategories;
+
+ private final DoubleProperty monthlySpent;
+ private final DoubleProperty monthlyRemaining;
+ private final DoubleProperty weeklySpent;
+ private final DoubleProperty weeklyRemaining;
+ private final DoubleProperty monthlyChange;
+ private final DoubleProperty weeklyChange;
+ private final DoubleProperty totalSpent;
+ private final DoubleProperty budgetPercentage;
+ private final DoubleProperty monthlyBudget;
+ private final DoubleProperty weeklyBudget;
+ private final LocalDate currentDate;
+
+ /**
+ * Initializes an AnalyticModelManager with the given expense tracker data
+ * and a given date which serves as a point of reference from which analytics
+ * are generated
+ */
+ public AnalyticModelManager(ReadOnlyExpenseTracker expenseTracker, LocalDate referenceDate) {
+ requireNonNull(expenseTracker);
+ requireNonNull(referenceDate);
+ allExpenses = expenseTracker.getExpenseList();
+ allCategories = expenseTracker.getCategoryList();
+ monthlyBudget = new SimpleDoubleProperty(0);
+ weeklyBudget = new SimpleDoubleProperty(0);
+ monthlySpent = new SimpleDoubleProperty(0);
+ monthlyRemaining = new SimpleDoubleProperty(0);
+ weeklySpent = new SimpleDoubleProperty(0);
+ weeklyRemaining = new SimpleDoubleProperty(0);
+ weeklyChange = new SimpleDoubleProperty(0);
+ monthlyChange = new SimpleDoubleProperty(0);
+ totalSpent = new SimpleDoubleProperty(0);
+ budgetPercentage = new SimpleDoubleProperty(0);
+ currentDate = referenceDate;
+
+ updateAllStatistics();
+ updateMonthlyBudgetProperty(expenseTracker.getBudgetForStats().get());
+ updateWeeklyBudgetProperty(expenseTracker.getBudgetForStats().get());
+
+ allExpenses.addListener((ListChangeListener) expenseChange -> {
+ updateAllStatistics();
+ });
+ expenseTracker.getBudgetForStats().addListener((observable, oldValue, newValue) -> {
+ updateMonthlyBudgetProperty(newValue);
+ updateWeeklyBudgetProperty(newValue);
+ updateAllStatistics();
+ });
+ }
+
+ /**
+ * Initializes an AnalyticModelManager with the given expense tracker data
+ * Date reference for analytics is taken to be the current date and time of construction
+ */
+ public AnalyticModelManager(ReadOnlyExpenseTracker expenseTracker) {
+ this(expenseTracker, LocalDate.now());
+ }
+
+
+ /**
+ * Calculates the total amount spent in the current month based
+ * on the filtered expenses list and updates the value of monthlySpent property
+ * @return DoubleProperty representing the monthly spent amount
+ */
+ @Override
+ public DoubleProperty getMonthlySpent() {
+ double total = 0;
+ int currentYear = currentDate.getYear();
+ int currentMonth = currentDate.getMonthValue();
+ LocalDate startOfMonth = LocalDate.of(currentYear, currentMonth, 1);
+ LocalDate endOfMonth = startOfMonth.with(TemporalAdjusters.lastDayOfMonth());
+ for (Expense expense: allExpenses) {
+ LocalDate expenseDate = expense.getDate();
+ if (!expenseDate.isBefore(startOfMonth) && !expenseDate.isAfter(endOfMonth)) {
+ total += expense.getAmount();
+ }
+ }
+ monthlySpent.set(total);
+ return monthlySpent;
+ }
+
+
+ /**
+ * Calculates remaining budget for the current month
+ * and updates the value of monthlyRemaining property
+ * @return DoubleProperty representing the remaining budget for the month
+ */
+ @Override
+ public DoubleProperty getMonthlyRemaining() {
+ // Do not calculate monthly remaining if budget is 0
+ if (monthlyBudget.get() == 0) {
+ monthlyRemaining.set(0);
+ return monthlyRemaining;
+ }
+ double remaining = monthlyBudget.get() - monthlySpent.get();
+ monthlyRemaining.set(remaining);
+ return monthlyRemaining;
+ }
+
+ /**
+ * Calculates total amount spent during the current week
+ * based on the filtered expenses list and updates the value of weeklySpent property
+ * @return DoubleProperty representing the total amount spent during the current week
+ */
+ @Override
+ public DoubleProperty getWeeklySpent() {
+ double total = 0;
+ LocalDate weekStart = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
+ LocalDate weekEnd = currentDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
+ for (Expense expense: allExpenses) {
+ LocalDate expenseDate = expense.getDate();
+ if (!expenseDate.isBefore(weekStart) && !expenseDate.isAfter(weekEnd)) {
+ total += expense.getAmount();
+ }
+ }
+ weeklySpent.set(total);
+ return weeklySpent;
+ }
+
+ /**
+ * Calculates remaining budget for the current week
+ * and updates the value of weeklyRemaining property
+ * @return DoubleProperty representing the remaining budget for the week
+ */
+ @Override
+ public DoubleProperty getWeeklyRemaining() {
+ // Do not calculate weekly remaining if budget is 0
+ if (monthlyBudget.get() == 0) {
+ weeklyRemaining.set(0);
+ return weeklyRemaining;
+ }
+ double remaining = weeklyBudget.get() - weeklySpent.get();
+ weeklyRemaining.set(remaining);
+ return weeklyRemaining;
+ }
+
+ /**
+ * Calculates percentage change in spending from the previous week
+ * to the current week and updates the value of weeklyChange property
+ * @return DoubleProperty representing percentage change
+ */
+ @Override
+ public DoubleProperty getWeeklyChange() {
+ // Do not calculate weekly change if budget is 0
+ if (monthlyBudget.get() == 0) {
+ weeklyChange.set(0);
+ return weeklyChange;
+ }
+ double previousWeekTotal = 0;
+ LocalDate previousWeekStart = currentDate.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)).minusWeeks(1);
+ LocalDate previousWeekEnd = currentDate.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)).minusDays(1);
+ for (Expense expense: allExpenses) {
+ LocalDate expenseDate = expense.getDate();
+ if (!expenseDate.isBefore(previousWeekStart) && !expenseDate.isAfter(previousWeekEnd)) {
+ previousWeekTotal += expense.getAmount();
+ }
+ }
+ double change;
+ if (previousWeekTotal == 0) {
+ change = 0;
+ } else {
+ change = (weeklySpent.get() - previousWeekTotal) / previousWeekTotal;
+ }
+ weeklyChange.set(change * 100);
+ return weeklyChange;
+ }
+
+ /**
+ * Calculates percentage change in spending from the previous month
+ * to the current month and updates the value of monthlyChange property.
+ * @return DoubleProperty representing the percentage change
+ */
+ @Override
+ public DoubleProperty getMonthlyChange() {
+ // Do not calculate monthly change if budget is 0
+ if (monthlyBudget.get() == 0) {
+ monthlyChange.set(0);
+ return monthlyChange;
+ }
+ double previousMonthTotal = 0;
+ LocalDate previousMonthStart = currentDate.withDayOfMonth(1).minusMonths(1);
+ LocalDate previousMonthEnd = previousMonthStart.with(TemporalAdjusters.lastDayOfMonth());
+ for (Expense expense: allExpenses) {
+ LocalDate expenseDate = expense.getDate();
+ if (!expenseDate.isBefore(previousMonthStart) && !expenseDate.isAfter(previousMonthEnd)) {
+ previousMonthTotal += expense.getAmount();
+ }
+ }
+ double change;
+ if (previousMonthTotal == 0) {
+ change = 0;
+ } else {
+ change = (monthlySpent.get() - previousMonthTotal) / previousMonthTotal;
+ }
+ monthlyChange.set(change * 100);
+ return monthlyChange;
+ }
+
+ /**
+ * Calculates the total amount of money spent on all expenses
+ * @return a DoubleProperty representing the total amount spent
+ */
+ @Override
+ public DoubleProperty getTotalSpent() {
+ double amount = allExpenses.stream().mapToDouble(Expense::getAmount).sum();
+ totalSpent.set(amount);
+ return totalSpent;
+ }
+
+ /**
+ * Calculates and returns the percentage of the monthly budget that has been spent so far
+ * The percentage is capped at 100% if it exceeds 100
+ * @return a DoubleProperty representing the percentage of budget spent
+ */
+ @Override
+ public DoubleProperty getBudgetPercentage() {
+ // Do not calculate budget percentage if budget is 0
+ if (monthlyBudget.get() == 0) {
+ budgetPercentage.set(0);
+ return budgetPercentage;
+ }
+ double percentage = (monthlySpent.get() / monthlyBudget.get()) * 100;
+ if (percentage > 100) {
+ percentage = 100;
+ }
+ budgetPercentage.set(percentage);
+ return budgetPercentage;
+ }
+
+ /**
+ * Updates the currently referenced monthly budget in the GUI
+ * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed.
+ * @param newBudget the new Budget object value which changed
+ */
+ @Override
+ public void updateMonthlyBudgetProperty(Budget newBudget) {
+ monthlyBudget.set(newBudget.getMonthlyBudget());
+ }
+
+ /**
+ * Updates the currently referenced weekly budget in the GUI
+ * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed.
+ * @param newBudget the new Budget object value which changed
+ */
+ @Override
+ public void updateWeeklyBudgetProperty(Budget newBudget) {
+ weeklyBudget.set(newBudget.getWeeklyBudget());
+ }
+
+ /**
+ * Returns the current value of the user's set monthly budget
+ */
+ @Override
+ public double getBudget() {
+ return monthlyBudget.get();
+ }
+
+ /**
+ * Gets the Property of the users current monthly budget
+ */
+ @Override
+ public DoubleProperty getMonthlyBudgetProperty() {
+ return monthlyBudget;
+ }
+
+ /**
+ * Returns a DoubleProperty representing analytics data for a given AnalyticsType.
+ * @param type an AnalyticsType enum that specifies which type of analytics data to return
+ * @return a DoubleProperty representing the requested analytics data
+ * @throws IllegalArgumentException if the given analytics type is not supported
+ */
+ @Override
+ public DoubleProperty getAnalyticsData(AnalyticsType type) throws IllegalArgumentException {
+ switch(type) {
+ case MONTHLY_SPENT:
+ return getMonthlySpent();
+ case MONTHLY_REMAINING:
+ return getMonthlyRemaining();
+ case WEEKLY_SPENT:
+ return getWeeklySpent();
+ case WEEKLY_REMAINING:
+ return getWeeklyRemaining();
+ case WEEKLY_CHANGE:
+ return getWeeklyChange();
+ case MONTHLY_CHANGE:
+ return getMonthlyChange();
+ case TOTAL_SPENT:
+ return getTotalSpent();
+ case BUDGET_PERCENTAGE:
+ return getBudgetPercentage();
+ default:
+ throw new IllegalArgumentException("Unsupported analytics type");
+ }
+ }
+
+ /**
+ * A convenience method to re-calculate and update all statistics
+ */
+ @Override
+ public void updateAllStatistics() {
+ getTotalSpent();
+ getMonthlySpent();
+ getWeeklySpent();
+ getMonthlyRemaining();
+ getWeeklyRemaining();
+ getWeeklyChange();
+ getMonthlyChange();
+ getBudgetPercentage();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // short circuit if same object
+ if (obj == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(obj instanceof AnalyticModelManager)) {
+ return false;
+ }
+
+ // state check
+ AnalyticModelManager other = (AnalyticModelManager) obj;
+ return allExpenses.equals(other.allExpenses)
+ && allCategories.equals(other.allCategories)
+ && currentDate.equals(other.currentDate);
+ }
+}
+
+
diff --git a/src/main/java/fasttrack/model/Budget.java b/src/main/java/fasttrack/model/Budget.java
new file mode 100644
index 00000000000..3babe8a757d
--- /dev/null
+++ b/src/main/java/fasttrack/model/Budget.java
@@ -0,0 +1,47 @@
+package fasttrack.model;
+
+
+/**
+ * Represents a budget which users of FastTrack can set.
+ */
+public class Budget {
+ private double monthBudget;
+
+ public Budget(double budget) {
+ this.monthBudget = budget;
+ }
+
+ /**
+ * Returns the monthlyBudget.
+ * @return double
+ */
+ public double getMonthlyBudget() {
+ return this.monthBudget;
+ }
+
+ /**
+ * Returns the weekly budget.
+ * @return double
+ */
+ public double getWeeklyBudget() {
+ return this.monthBudget / 4;
+ }
+
+
+ @Override
+ public String toString() {
+ return Double.toString(this.monthBudget);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof Budget // instanceof handles nulls
+ && monthBudget == ((Budget) other).monthBudget);
+ }
+
+ @Override
+ public int hashCode() {
+ return Double.hashCode(monthBudget);
+ }
+}
diff --git a/src/main/java/fasttrack/model/ExpenseTracker.java b/src/main/java/fasttrack/model/ExpenseTracker.java
new file mode 100644
index 00000000000..4eaa1393a8b
--- /dev/null
+++ b/src/main/java/fasttrack/model/ExpenseTracker.java
@@ -0,0 +1,271 @@
+package fasttrack.model;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+import java.util.Objects;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.category.UniqueCategoryList;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.ExpenseList;
+import fasttrack.model.expense.RecurringExpenseList;
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+
+
+/**
+ * Wraps all data at the expense tracker level
+ * Duplicate categories are not allowed (by .isSameCategory comparison)
+ */
+public class ExpenseTracker implements ReadOnlyExpenseTracker {
+
+ private final UniqueCategoryList categories;
+ private final ExpenseList expenses;
+ private final RecurringExpenseList recurringGenerators;
+ private final ObjectProperty simpleBudget;
+
+ /*
+ * The 'unusual' code block below is a non-static initialization block,
+ * sometimes used to avoid duplication
+ * between constructors. See
+ * https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html
+ *
+ * Note that non-static init blocks are not recommended to use. There are other
+ * ways to avoid duplication
+ * among constructors.
+ */
+ {
+ categories = new UniqueCategoryList();
+ expenses = new ExpenseList();
+ simpleBudget = new SimpleObjectProperty<>(new Budget(0));
+ recurringGenerators = new RecurringExpenseList();
+ }
+
+ public ExpenseTracker() {
+ }
+
+ /**
+ * Creates an ExpenseTracker using the data in the {@code toBeCopied}
+ */
+ public ExpenseTracker(ReadOnlyExpenseTracker toBeCopied) {
+ this();
+ resetData(toBeCopied);
+ }
+
+ //// list overwrite operations
+
+ /**
+ * Replaces the contents of the category list with {@code categories}.
+ * {@code categories} must not contain duplicate categories.
+ */
+ public void setCategories(List categories) {
+ this.categories.setCategoryList(categories);
+ }
+
+ /**
+ * Replaces the contents of the expense list with {@code expenses}.
+ */
+ public void setExpenses(List expenses) {
+ this.expenses.setExpenseList(expenses);
+ }
+
+ public void setBudget(Budget budget) {
+ this.simpleBudget.set(budget);
+ }
+
+ public void setRecurringExpenseGenerators(List recurringExpenseGenerators) {
+ this.recurringGenerators.setRecurringExpenseList(recurringExpenseGenerators);
+ }
+
+ /**
+ * Resets the existing data of this {@code ExpenseTracker} with {@code newData}.
+ */
+ public void resetData(ReadOnlyExpenseTracker newData) {
+ requireNonNull(newData);
+ setExpenses(newData.getExpenseList());
+ setCategories(newData.getCategoryList());
+ setBudget(newData.getBudget());
+ setRecurringExpenseGenerators(newData.getRecurringExpenseGenerators());
+ generateRetroactiveExpenses();
+ expenses.sortList();
+ cleanupExpiredRecurringExpenses();
+ }
+
+ /**
+ * Adds expenses retroactively for recurring expenses which have starting dates that need to be added.
+ */
+ public void generateRetroactiveExpenses() {
+ for (RecurringExpenseManager generators : recurringGenerators.getRecurringExpenseList()) {
+ for (Expense expense : generators.getExpenses()) {
+ addExpense(expense);
+ }
+ }
+ expenses.sortList();
+ }
+
+ /**
+ * Removes RecurringExpenseManager objects that are expired.
+ */
+ public void cleanupExpiredRecurringExpenses() {
+ recurringGenerators.cleanupExpiredGenerators();
+ }
+
+ //// category-level operations
+ /**
+ * Returns true if the given category exists in the list.
+ * @param category The category to check for existence in the list.
+ * @return true if the category exists in the list and false otherwise.
+ */
+ public boolean hasCategory(Category category) {
+ requireNonNull(category);
+ return categories.contains(category);
+ }
+
+ /**
+ * Adds a category to the expense tracker.
+ * The category must not already exist in the expense tracker.
+ */
+ public void addCategory(Category toAdd) {
+ categories.add(toAdd);
+ }
+
+ /**
+ * Deletes the given category {@code key} in the UniqueCategoryList.
+ * Replaces all expenses with {@code key} with the MiscellaneousCategory object.
+ * @param key
+ */
+ public void removeCategory(Category key) {
+ categories.remove(key);
+ expenses.replaceDeletedCategory(key);
+ recurringGenerators.replaceDeletedCategory(key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(categories, expenses);
+ }
+
+ public Category getCategoryInstance(Category category) {
+ for (Category c : categories.asUnmodifiableList()) {
+ if (category.equals(c)) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ //// util methods
+
+ @Override
+ public String toString() {
+ return expenses.asUnmodifiableList().size() + " expenses";
+ }
+
+ @Override
+ public ObservableList getCategoryList() {
+ return categories.asUnmodifiableList();
+ }
+
+ //// expense-level operations
+
+ @Override
+ public ObservableList getExpenseList() {
+ return expenses.asUnmodifiableList();
+ }
+
+ @Override
+ public ObservableList getRecurringExpenseGenerators() {
+ return recurringGenerators.asUnmodifiableList();
+ }
+ @Override
+ public Budget getBudget() {
+ return this.simpleBudget.get();
+ }
+
+ @Override
+ public ObjectProperty getBudgetForStats() {
+ return this.simpleBudget;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof ExpenseTracker // instanceof handles nulls
+ && expenses.equals(((ExpenseTracker) other).expenses)
+ && categories.equals(((ExpenseTracker) other).categories));
+ }
+
+ /**
+ * Adds an expense to the expense tracker.
+ * @param expense to be added.
+ */
+ public void addExpense(Expense expense) {
+ expenses.add(expense);
+ expenses.sortList();
+ }
+
+ /**
+ * Deletes an expense from the expense tracker.
+ * @param expense to be deleted.
+ */
+ public void removeExpense(Expense expense) {
+ expenses.remove(expense);
+ expenses.sortList();
+ }
+
+ /**
+ * Sets an expense at the specified index.
+ * @param index index to be used.
+ * @param expense expense to be used to overwrite the previous expense.
+ */
+ public void setExpense(int index, Expense expense) {
+ expenses.set(index, expense);
+ expenses.sortList();
+ }
+
+ public void setExpense(Expense target, Expense editedExpense) {
+ expenses.setExpense(target, editedExpense);
+ expenses.sortList();
+ }
+
+ /**
+ * Delete all Expense.
+ */
+ public void clearExpense() {
+ expenses.clear();
+ }
+
+ public void clearCategory() {
+ categories.clear();
+ }
+
+ public void clearRecurringExpense() {
+ recurringGenerators.clear();
+ }
+
+ /**
+ * Returns true if the given expense exists in the list.
+ * @param expense The expense to check for existence in the list.
+ * @return true if the expense exists in the list and false otherwise.
+ */
+ public boolean hasExpense(Expense expense) {
+ requireNonNull(expense);
+ return expenses.contains(expense);
+ }
+
+ public boolean hasRecurringExpense(RecurringExpenseManager recurringExpenseManager) {
+ return recurringGenerators.contains(recurringExpenseManager);
+ }
+
+ public void addRecurringGenerator(RecurringExpenseManager generator) {
+ recurringGenerators.addRecurringExpense(generator);
+ }
+
+
+ public void removeRecurringExpense(RecurringExpenseManager recurringExpenseManager) {
+ recurringGenerators.removeRecurringExpense(recurringExpenseManager);
+ }
+}
diff --git a/src/main/java/fasttrack/model/Model.java b/src/main/java/fasttrack/model/Model.java
new file mode 100644
index 00000000000..23438090518
--- /dev/null
+++ b/src/main/java/fasttrack/model/Model.java
@@ -0,0 +1,204 @@
+package fasttrack.model;
+
+import java.nio.file.Path;
+import java.util.function.Predicate;
+
+import fasttrack.commons.core.GuiSettings;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+
+/**
+ * The API of the DataModel component.
+ */
+public interface Model {
+
+ /** {@code Predicate} that always evaluate to true */
+ Predicate PREDICATE_SHOW_ALL_EXPENSES = unused -> true;
+ Predicate PREDICATE_SHOW_ALL_CATEGORY = unused -> true;
+
+ /**
+ * Replaces user prefs data with the data in {@code userPrefs}.
+ */
+ void setUserPrefs(ReadOnlyUserPrefs userPrefs);
+
+ /**
+ * Returns the user prefs.
+ */
+ ReadOnlyUserPrefs getUserPrefs();
+
+ /**
+ * Returns the user prefs' GUI settings.
+ */
+ GuiSettings getGuiSettings();
+
+ /**
+ * Sets the user prefs' GUI settings.
+ */
+ void setGuiSettings(GuiSettings guiSettings);
+
+ /**
+ * Returns the user prefs' expense tracker file path.
+ */
+ Path getExpenseTrackerFilePath();
+
+ /**
+ * Sets the user prefs' expense tracker file path.
+ */
+ void setExpenseTrackerFilePath(Path expenseTrackerFilePath);
+
+ /**
+ * Replaces expenseTracker data with the data in {@code expenseTracker}.
+ */
+ void setExpenseTracker(ReadOnlyExpenseTracker expenseTracker);
+
+ /** Returns the ExpenseTracker */
+ ReadOnlyExpenseTracker getExpenseTracker();
+
+ SimpleObjectProperty getAppliedTimeSpanFilter();
+
+ SimpleObjectProperty getAppliedCategoryFilter();
+
+ void updateTimeSpanFilter(ParserUtil.Timespan timeSpan);
+
+ void updateCategoryFilter(Category category);
+
+ // Expense accessor functions
+
+ /**
+ * Adds the given expense to the expense tracker
+ * @param expense the new expense to add
+ */
+ void addExpense(Expense expense);
+
+ /**
+ * Deletes the given expense.
+ * The expense must exist in the ExpenseTracker.
+ * @param expense the expense to delete
+ */
+ void deleteExpense(Expense expense);
+
+ /**
+ * Delete all expense.
+ */
+ void clearExpense();
+
+
+ /**
+ * Replaces the Expense in the expense list at the given index.
+ * @param index
+ * @param expense
+ */
+ void setExpense(int index, Expense expense);
+
+ /**
+ * Replaces the given expense {@code target} with {@code editedExpense}.
+ * {@code target} must exist in the expense list
+ */
+ void setExpense(Expense target, Expense editedExpense);
+
+
+ /**
+ * Indicates if an expense exists in the expense list
+ * @param expense the expense to check for
+ */
+ boolean hasExpense(Expense expense);
+
+ /**
+ * 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);
+
+ /** Returns an unmodifiable view of the filtered expense list */
+ ObservableList getFilteredExpenseList();
+
+ // Category accessor functions
+
+ /**
+ * Deletes the given expense.
+ * @param target the category to delete
+ */
+ void deleteCategory(Category target);
+
+ /**
+ * Delete all Category.
+ */
+ void clearCategory();
+
+ /**
+ * Adds the given category to the category list.
+ * @param toAdd the category to add
+ */
+ void addCategory(Category toAdd);
+
+
+ /**
+ * Indicates if a category exists in the category list
+ * @param category the category to check for
+ */
+ boolean hasCategory(Category category);
+
+ /** Returns an unmodifiable view of the category list */
+ ObservableList getFilteredCategoryList();
+
+ /**
+ * Updates the filter of the filtered category list to filter by the given
+ * {@code predicate}.
+ * @throws NullPointerException if {@code predicate} is null.
+ */
+ void updateFilteredCategoryList(Predicate predicate);
+
+ /**
+ * Returns a reference to the instance of category
+ * matching the category name in the category list
+ * @param category the category to check for
+ * @return the category instance if it exists, and null if it does not
+ */
+ Category getCategoryInstance(Category category);
+
+ /**
+ * Sets budget for FastTrack.
+ * @param budget
+ */
+ void setBudget(Budget budget);
+
+ /**
+ * Indicates if a RecurringExpense exists in the RecurringExpenseList
+ * @param recurringExpense the RecurringExpense to check for
+ */
+ boolean hasRecurringExpense(RecurringExpenseManager recurringExpense);
+
+ /**
+ * Adds a RecurringExpense to the RecurringExpense list.
+ * @param recurringExpenseManager the Recurring expense to add.
+ */
+ void addRecurringGenerator(RecurringExpenseManager recurringExpenseManager);
+
+ /** Returns an unmodifiable view of the recurring expense list */
+ ObservableList getRecurringExpenseGenerators();
+
+ /**
+ * Updates the filter of the filtered recurring expense manager list to filter by the given
+ * {@code predicate}.
+ * @throws NullPointerException if {@code predicate} is null.
+ */
+ void updateFilteredRecurringGenerators(Predicate predicate);
+
+ /**
+ * Delete all recurring expense generators.
+ */
+ void clearRecurringExpenseGenerator();
+
+ /**
+ * Deletes the target {@code RecurringExpense} from the recurring expense list.
+ * @param recurringExpenseManager the recurring expense to be deleted.
+ */
+ void deleteRecurringExpense(RecurringExpenseManager recurringExpenseManager);
+
+ void addRetroactiveExpenses();
+}
diff --git a/src/main/java/fasttrack/model/ModelManager.java b/src/main/java/fasttrack/model/ModelManager.java
new file mode 100644
index 00000000000..e9925787646
--- /dev/null
+++ b/src/main/java/fasttrack/model/ModelManager.java
@@ -0,0 +1,303 @@
+package fasttrack.model;
+
+import static fasttrack.commons.util.CollectionUtil.requireAllNonNull;
+import static java.util.Objects.requireNonNull;
+
+import java.nio.file.Path;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+
+import fasttrack.commons.core.GuiSettings;
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+
+/**
+ * Represents the in-memory model of the expense tracker data.
+ */
+public class ModelManager implements Model {
+ private static final Logger logger = LogsCenter.getLogger(ModelManager.class);
+
+ private final ExpenseTracker expenseTracker;
+ private final UserPrefs userPrefs;
+ private final FilteredList filteredExpenses;
+ private final FilteredList filteredCategories;
+ private final FilteredList filteredRecurringExpense;
+
+ private SimpleObjectProperty appliedCategoryFilter =
+ new SimpleObjectProperty<>(null);
+ private SimpleObjectProperty appliedTimeSpanFilter =
+ new SimpleObjectProperty<>(ParserUtil.Timespan.ALL);
+
+ /**
+ * Initializes a ModelManager with the given expenseTracker and userPrefs.
+ */
+ public ModelManager(ReadOnlyExpenseTracker expenseTracker, ReadOnlyUserPrefs userPrefs) {
+ requireAllNonNull(expenseTracker, userPrefs);
+ logger.fine("Initializing with expense tracker: " + expenseTracker + " and user prefs " + userPrefs);
+ this.expenseTracker = new ExpenseTracker(expenseTracker);
+ this.userPrefs = new UserPrefs(userPrefs);
+ filteredExpenses = new FilteredList<>(this.expenseTracker.getExpenseList());
+ filteredCategories = new FilteredList<>(this.expenseTracker.getCategoryList());
+ filteredRecurringExpense = new FilteredList<>(this.expenseTracker.getRecurringExpenseGenerators());
+ }
+
+ public ModelManager() {
+ this(new ExpenseTracker(), new UserPrefs());
+ }
+
+
+ // =========== UserPrefs
+ // ==================================================================================
+
+ @Override
+ public void setUserPrefs(ReadOnlyUserPrefs userPrefs) {
+ requireNonNull(userPrefs);
+ this.userPrefs.resetData(userPrefs);
+ }
+
+ @Override
+ public ReadOnlyUserPrefs getUserPrefs() {
+ return userPrefs;
+ }
+
+ @Override
+ public GuiSettings getGuiSettings() {
+ return userPrefs.getGuiSettings();
+ }
+
+ @Override
+ public void setGuiSettings(GuiSettings guiSettings) {
+ requireNonNull(guiSettings);
+ userPrefs.setGuiSettings(guiSettings);
+ }
+
+ @Override
+ public Path getExpenseTrackerFilePath() {
+ return userPrefs.getExpenseTrackerFilePath();
+ }
+
+ @Override
+ public void setExpenseTrackerFilePath(Path expenseTrackerFilePath) {
+ requireNonNull(expenseTrackerFilePath);
+ userPrefs.setExpenseTrackerFilePath(expenseTrackerFilePath);
+ }
+
+ // =========== ExpenseTracker
+ // ================================================================================
+
+ @Override
+ public void setExpenseTracker(ReadOnlyExpenseTracker expenseTracker) {
+ this.expenseTracker.resetData(expenseTracker);
+ }
+
+ @Override
+ public ReadOnlyExpenseTracker getExpenseTracker() {
+ return expenseTracker;
+ }
+
+ // =========== Expenses List Accessors
+ // =============================================================
+ @Override
+ public boolean equals(Object obj) {
+ // short circuit if same object
+ if (obj == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(obj instanceof ModelManager)) {
+ return false;
+ }
+
+ // state check
+ ModelManager other = (ModelManager) obj;
+ return expenseTracker.equals(other.expenseTracker)
+ && userPrefs.equals(other.userPrefs)
+ && filteredExpenses.equals(other.filteredExpenses)
+ && filteredCategories.equals(other.filteredCategories)
+ && filteredRecurringExpense.equals(other.filteredRecurringExpense);
+ }
+
+ @Override
+ public SimpleObjectProperty getAppliedTimeSpanFilter() {
+ return appliedTimeSpanFilter;
+ }
+
+ @Override
+ public SimpleObjectProperty getAppliedCategoryFilter() {
+ return appliedCategoryFilter;
+ }
+
+ @Override
+ public void updateTimeSpanFilter(ParserUtil.Timespan timeSpan) {
+ appliedTimeSpanFilter.set(timeSpan);
+ }
+
+ @Override
+ public void updateCategoryFilter(Category category) {
+ appliedCategoryFilter.set(category);
+ }
+
+ // =========== Category List Accessors
+ // =============================================================
+
+ @Override
+ public ObservableList getFilteredCategoryList() {
+ return filteredCategories;
+ }
+
+ @Override
+ public void updateFilteredCategoryList(Predicate predicate) {
+ requireNonNull(predicate);
+ filteredCategories.setPredicate(predicate);
+ }
+
+ /**
+ * Indicates if a category exists in the category list
+ * @param category the category to check for
+ */
+ @Override
+ public boolean hasCategory(Category category) {
+ requireNonNull(category);
+ return expenseTracker.hasCategory(category);
+ }
+
+ @Override
+ public void addCategory(Category toAdd) {
+ expenseTracker.addCategory(toAdd);
+ }
+
+ @Override
+ public void deleteCategory(Category target) {
+ expenseTracker.removeCategory(target);
+ updateFilteredCategoryList(PREDICATE_SHOW_ALL_CATEGORY);
+ updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES);
+ }
+
+ @Override
+ public void clearCategory() {
+ expenseTracker.clearCategory();
+ }
+
+ @Override
+ public Category getCategoryInstance(Category category) {
+ if (hasCategory(category)) {
+ return expenseTracker.getCategoryInstance(category);
+ }
+ return null;
+ }
+
+
+ // =========== Filtered Expense List Accessors
+ // =============================================================
+
+ /**
+ * Returns an unmodifiable view of the list of {@code Expense} backed by the
+ * internal list of
+ * {@code versionedAddressBook}
+ */
+ @Override
+ public ObservableList getFilteredExpenseList() {
+ return filteredExpenses;
+ }
+
+ /**
+ * Replaces the given expense {@code target} with {@code editedExpense}.
+ * {@code target} must exist in the expense list
+ */
+ @Override
+ public void setExpense(Expense target, Expense editedExpense) {
+ expenseTracker.setExpense(target, editedExpense);
+ }
+
+ @Override
+ public void setExpense(int index, Expense newExpense) {
+ expenseTracker.setExpense(index, newExpense);
+ }
+
+ @Override
+ public void updateFilteredExpensesList(Predicate predicate) {
+ requireNonNull(predicate);
+ filteredExpenses.setPredicate(predicate);
+ }
+
+ @Override
+ public void addExpense(Expense expense) {
+ expenseTracker.addExpense(expense);
+ updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES);
+ }
+
+ @Override
+ public void deleteExpense(Expense expense) {
+ expenseTracker.removeExpense(expense);
+ updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES);
+ }
+
+ @Override
+ public void clearExpense() {
+ expenseTracker.clearExpense();
+ }
+
+ /**
+ * Indicates if an expense exists in the expense list
+ * @param expense the expense to check for
+ */
+ @Override
+ public boolean hasExpense(Expense expense) {
+ requireNonNull(expense);
+ return expenseTracker.hasExpense(expense);
+ }
+
+ @Override
+ public void setBudget(Budget budget) {
+ expenseTracker.setBudget(budget);
+ }
+
+ // =========== Recurring Expense Manager List Accessors
+ // =============================================================
+
+ @Override
+ public boolean hasRecurringExpense(RecurringExpenseManager recurringExpenseManager) {
+ return expenseTracker.hasRecurringExpense(recurringExpenseManager);
+ }
+
+ @Override
+ public void addRecurringGenerator(RecurringExpenseManager recurringExpenseManager) {
+ expenseTracker.addRecurringGenerator(recurringExpenseManager);
+ }
+
+ @Override
+ public ObservableList getRecurringExpenseGenerators() {
+ return expenseTracker.getRecurringExpenseGenerators();
+ }
+
+ @Override
+ public void updateFilteredRecurringGenerators(Predicate predicate) {
+ requireNonNull(predicate);
+ filteredRecurringExpense.setPredicate(predicate);
+ }
+
+ /**
+ * Delete all recurring expense generators.
+ */
+ @Override
+ public void clearRecurringExpenseGenerator() {
+ expenseTracker.clearRecurringExpense();
+ }
+
+ @Override
+ public void deleteRecurringExpense(RecurringExpenseManager recurringExpenseManager) {
+ expenseTracker.removeRecurringExpense(recurringExpenseManager);
+ }
+
+ @Override
+ public void addRetroactiveExpenses() {
+ expenseTracker.generateRetroactiveExpenses();
+ }
+}
diff --git a/src/main/java/fasttrack/model/ReadOnlyExpenseTracker.java b/src/main/java/fasttrack/model/ReadOnlyExpenseTracker.java
new file mode 100644
index 00000000000..02bd307892c
--- /dev/null
+++ b/src/main/java/fasttrack/model/ReadOnlyExpenseTracker.java
@@ -0,0 +1,31 @@
+package fasttrack.model;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.beans.property.ObjectProperty;
+import javafx.collections.ObservableList;
+
+/**
+ * Unmodifiable view of an expense tracker
+ */
+public interface ReadOnlyExpenseTracker {
+
+ /**
+ * Returns an unmodifiable view of the category list.
+ * This list will not contain any duplicate categories.
+ */
+ ObservableList getCategoryList();
+
+ /**
+ * Returns an unmodifiable view of the expense list.
+ */
+ ObservableList getExpenseList();
+
+ Budget getBudget();
+
+ ObjectProperty getBudgetForStats();
+
+ ObservableList getRecurringExpenseGenerators();
+
+}
diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/fasttrack/model/ReadOnlyUserPrefs.java
similarity index 57%
rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java
rename to src/main/java/fasttrack/model/ReadOnlyUserPrefs.java
index befd58a4c73..4425b97cd3e 100644
--- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java
+++ b/src/main/java/fasttrack/model/ReadOnlyUserPrefs.java
@@ -1,8 +1,8 @@
-package seedu.address.model;
+package fasttrack.model;
import java.nio.file.Path;
-import seedu.address.commons.core.GuiSettings;
+import fasttrack.commons.core.GuiSettings;
/**
* Unmodifiable view of user prefs.
@@ -11,6 +11,6 @@ public interface ReadOnlyUserPrefs {
GuiSettings getGuiSettings();
- Path getAddressBookFilePath();
+ Path getExpenseTrackerFilePath();
}
diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/fasttrack/model/UserPrefs.java
similarity index 68%
rename from src/main/java/seedu/address/model/UserPrefs.java
rename to src/main/java/fasttrack/model/UserPrefs.java
index 25a5fd6eab9..45a18c88239 100644
--- a/src/main/java/seedu/address/model/UserPrefs.java
+++ b/src/main/java/fasttrack/model/UserPrefs.java
@@ -1,4 +1,4 @@
-package seedu.address.model;
+package fasttrack.model;
import static java.util.Objects.requireNonNull;
@@ -6,7 +6,7 @@
import java.nio.file.Paths;
import java.util.Objects;
-import seedu.address.commons.core.GuiSettings;
+import fasttrack.commons.core.GuiSettings;
/**
* Represents User's preferences.
@@ -14,7 +14,7 @@
public class UserPrefs implements ReadOnlyUserPrefs {
private GuiSettings guiSettings = new GuiSettings();
- private Path addressBookFilePath = Paths.get("data" , "addressbook.json");
+ private Path expenseTrackerFilePath = Paths.get("data" , "fastTrack.json");
/**
* Creates a {@code UserPrefs} with default values.
@@ -35,9 +35,10 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) {
public void resetData(ReadOnlyUserPrefs newUserPrefs) {
requireNonNull(newUserPrefs);
setGuiSettings(newUserPrefs.getGuiSettings());
- setAddressBookFilePath(newUserPrefs.getAddressBookFilePath());
+ setExpenseTrackerFilePath(newUserPrefs.getExpenseTrackerFilePath());
}
+ @Override
public GuiSettings getGuiSettings() {
return guiSettings;
}
@@ -47,13 +48,14 @@ public void setGuiSettings(GuiSettings guiSettings) {
this.guiSettings = guiSettings;
}
- public Path getAddressBookFilePath() {
- return addressBookFilePath;
+ @Override
+ public Path getExpenseTrackerFilePath() {
+ return expenseTrackerFilePath;
}
- public void setAddressBookFilePath(Path addressBookFilePath) {
- requireNonNull(addressBookFilePath);
- this.addressBookFilePath = addressBookFilePath;
+ public void setExpenseTrackerFilePath(Path expenseTrackerFilePath) {
+ requireNonNull(expenseTrackerFilePath);
+ this.expenseTrackerFilePath = expenseTrackerFilePath;
}
@Override
@@ -68,19 +70,19 @@ public boolean equals(Object other) {
UserPrefs o = (UserPrefs) other;
return guiSettings.equals(o.guiSettings)
- && addressBookFilePath.equals(o.addressBookFilePath);
+ && expenseTrackerFilePath.equals(o.expenseTrackerFilePath);
}
@Override
public int hashCode() {
- return Objects.hash(guiSettings, addressBookFilePath);
+ return Objects.hash(guiSettings, expenseTrackerFilePath);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Gui Settings : " + guiSettings);
- sb.append("\nLocal data file location : " + addressBookFilePath);
+ sb.append("\nLocal data file location : " + expenseTrackerFilePath);
return sb.toString();
}
diff --git a/src/main/java/fasttrack/model/category/Category.java b/src/main/java/fasttrack/model/category/Category.java
new file mode 100644
index 00000000000..447d878829e
--- /dev/null
+++ b/src/main/java/fasttrack/model/category/Category.java
@@ -0,0 +1,64 @@
+package fasttrack.model.category;
+
+import java.util.Objects;
+
+/**
+ * Category class to represent categories that expenses are grouped under.
+ */
+public abstract class Category {
+
+ public static final String MESSAGE_CONSTRAINTS = "Category names should be alphanumeric";
+ public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*";
+
+ protected String categoryName;
+ protected String summary;
+
+ /**
+ * Constructor for Category class.
+ * @param categoryName Name of the category
+ * @param summary Short description of the category
+ */
+ public Category(String categoryName, String summary) {
+ this.categoryName = categoryName;
+ this.summary = summary;
+ }
+
+ /**
+ * Returns true if a given string is a valid category name.
+ */
+ public static boolean isValidCategoryName(String categoryName) {
+ return categoryName.matches(VALIDATION_REGEX) && !categoryName.isBlank();
+ }
+
+ public String getCategoryName() {
+ return this.categoryName;
+ };
+
+ public String getSummary() {
+ return this.summary;
+ };
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof Category)) {
+ return false;
+ }
+
+ Category otherCategory = (Category) other;
+ return otherCategory.getCategoryName().strip().toLowerCase().equals(getCategoryName().strip().toLowerCase());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(categoryName, summary);
+ }
+
+ @Override
+ public String toString() {
+ return this.categoryName;
+ }
+}
diff --git a/src/main/java/fasttrack/model/category/MiscellaneousCategory.java b/src/main/java/fasttrack/model/category/MiscellaneousCategory.java
new file mode 100644
index 00000000000..9654c22cf2c
--- /dev/null
+++ b/src/main/java/fasttrack/model/category/MiscellaneousCategory.java
@@ -0,0 +1,31 @@
+package fasttrack.model.category;
+
+/**
+* Category class to represent categories that expenses are not grouped into a
+* specific category.
+*/
+public class MiscellaneousCategory extends Category {
+ /**
+ * Constructor for MiscellaneousCategory class.
+ */
+ public MiscellaneousCategory() {
+ super("Misc", "Placeholder Description");
+ }
+
+ @Override
+ public String toString() {
+ return "Miscellaneous";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof MiscellaneousCategory // instanceof handles nulls
+ && this.categoryName.equals(((MiscellaneousCategory) other).categoryName)); // state check
+ }
+
+ @Override
+ public int hashCode() {
+ return categoryName.hashCode();
+ }
+}
diff --git a/src/main/java/fasttrack/model/category/UniqueCategoryList.java b/src/main/java/fasttrack/model/category/UniqueCategoryList.java
new file mode 100644
index 00000000000..a5ff20913cd
--- /dev/null
+++ b/src/main/java/fasttrack/model/category/UniqueCategoryList.java
@@ -0,0 +1,135 @@
+package fasttrack.model.category;
+
+import static fasttrack.commons.util.CollectionUtil.requireAllNonNull;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * A list of categories that enforces uniqueness between its elements and does not allow nulls.
+ */
+public class UniqueCategoryList implements Iterable {
+
+ private final ObservableList internalListOfCategories = FXCollections.observableArrayList();
+ private final ObservableList internalUnmodifiableList = FXCollections
+ .unmodifiableObservableList(internalListOfCategories);
+
+ /**
+ * Returns true if the list contains an equivalent category as the given argument.
+ * @param category Category to check for
+ */
+ public boolean contains(Category category) {
+ requireNonNull(category);
+ return internalListOfCategories.stream().anyMatch(category::equals);
+ }
+
+ /**
+ * Adds a category to the list.
+ * @param newCategory Category to add
+ */
+ public void add(Category newCategory) {
+ requireNonNull(newCategory);
+
+ if (contains(newCategory)) {
+ //Throw an exception here later
+ }
+ internalListOfCategories.add(newCategory);
+ }
+
+ /**
+ * Replaces the category {@code target} in the list with {@code editedCategory}.
+ * {@code target} must exist in the list.
+ * @param category Category to remove
+ */
+ public void remove(Category category) {
+ requireNonNull(category);
+
+ if (!internalListOfCategories.remove(category)) {
+ //Throw an exception here later
+ }
+ }
+
+ /**
+ * Delete all Category.
+ */
+ public void clear() {
+ internalListOfCategories.clear();
+ internalUnmodifiableList.clear();
+ }
+
+ /**
+ * Replaces the category {@code target} in the list with {@code editedCategory}.
+ * {@code target} must exist in the list.
+ * The category identity of {@code editedCategory} must not be
+ * the same as another existing category in the list.
+ * @param replacementList List of categories to replace the current list
+ */
+ public void setCategoryList(UniqueCategoryList replacementList) {
+ requireNonNull(replacementList);
+ internalListOfCategories.setAll(replacementList.internalListOfCategories);
+ }
+
+ /**
+ * Replaces the category {@code target} in the list with {@code editedCategory}.
+ * {@code target} must exist in the list.
+ * The category identity of {@code editedCategory} must not be
+ * the same as another existing category in the list.
+ * @param listOfCategories List of categories to replace the current list
+ */
+ public void setCategoryList(List listOfCategories) {
+ requireAllNonNull(listOfCategories);
+ if (!categoriesAreUnique(listOfCategories)) {
+ //Throw an exception here
+ }
+ internalListOfCategories.setAll(listOfCategories);
+ }
+
+ /**
+ * Returns the backing list as an unmodifiable {@code ObservableList}.
+ * This list will not contain any null categories.
+ * @param listOfCategories List of categories to check for uniqueness
+ */
+ public boolean categoriesAreUnique(List listOfCategories) {
+ for (int i = 0; i < listOfCategories.size(); i++) {
+ for (int j = i + 1; j < listOfCategories.size(); j++) {
+ if (listOfCategories.get(i).equals(listOfCategories.get(j))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ /**
+ * Returns the backing list as an unmodifiable {@code ObservableList}.
+ * This list will not contain any null categories.
+ */
+ public ObservableList asUnmodifiableList() {
+ return this.internalUnmodifiableList;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this.internalListOfCategories.iterator();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof UniqueCategoryList)) {
+ return false;
+ }
+ UniqueCategoryList otherInUniqueList = (UniqueCategoryList) other;
+ return this.internalListOfCategories.equals(otherInUniqueList.internalListOfCategories);
+ }
+
+ @Override
+ public int hashCode() {
+ return internalListOfCategories.hashCode();
+ }
+}
diff --git a/src/main/java/fasttrack/model/category/UserDefinedCategory.java b/src/main/java/fasttrack/model/category/UserDefinedCategory.java
new file mode 100644
index 00000000000..309ddc7ddbf
--- /dev/null
+++ b/src/main/java/fasttrack/model/category/UserDefinedCategory.java
@@ -0,0 +1,24 @@
+package fasttrack.model.category;
+
+/**
+ * User-defined category class which allows users to customize their own
+ * categories to use.
+ */
+public class UserDefinedCategory extends Category {
+ /**
+ * Constructor for UserDefinedCategory class.
+ * @param categoryName Name of the category
+ * @param summary Short description of the category
+ */
+ public UserDefinedCategory(String categoryName, String summary) {
+ super(categoryName, summary);
+ }
+
+ public void setCategoryName(String categoryName) {
+ this.categoryName = categoryName.replaceAll("\\s+", " ");;
+ }
+
+ public void setDescription(String summary) {
+ this.summary = summary.replaceAll("\\s+", " ");
+ }
+}
diff --git a/src/main/java/fasttrack/model/expense/Expense.java b/src/main/java/fasttrack/model/expense/Expense.java
new file mode 100644
index 00000000000..c3cc2417212
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/Expense.java
@@ -0,0 +1,150 @@
+package fasttrack.model.expense;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+import fasttrack.model.category.Category;
+
+/**
+ * Represents an Expense in the expense tracker.
+ * Guarantees: details are present and not null, field values are validated,
+ * immutable.
+ * @author shirsho-12
+ * @version 1.0
+ */
+public class Expense {
+
+ public static final String MESSAGE_CONSTRAINTS = "Expense names should be alphanumeric";
+ public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*";
+
+ private String name;
+ private Price amount;
+ private LocalDate date;
+ private Category category;
+
+ /**
+ * Constructor for Expense class.
+ * @param name Name of the expense
+ * @param amount Amount of the expense
+ * @param date Date of the expense
+ * @param category Category of the expense
+ */
+ public Expense(String name, Price amount, LocalDate date, Category category) {
+ this.name = name;
+ this.amount = amount;
+ this.date = date;
+ this.category = category;
+ }
+
+ /**
+ * Constructor for Expense class.
+ * @param name Name of the expense
+ * @param amount Amount of the expense
+ * @param date Date of the expense
+ * @param category Category of the expense
+ */
+ public Expense(String name, double amount, LocalDate date, Category category) {
+ this.name = name;
+ this.amount = new Price(amount);
+ this.date = date;
+ this.category = category;
+ }
+
+ /**
+ * Constructor for Expense class.
+ * @param name Name of the expense
+ * @param amount Amount of the expense
+ * @param date Date of the expense
+ * @param category Category of the expense
+ */
+ public Expense(String name, String amount, LocalDate date, Category category) {
+ this.name = name;
+ this.amount = new Price(amount);
+ this.date = date;
+ this.category = category;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public double getAmount() {
+ return amount.getPriceAsDouble();
+ }
+
+ public LocalDate getDate() {
+ return date;
+ }
+
+ public String getFormattedDate() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ return date.format(formatter);
+ }
+
+ public Category getCategory() {
+ return category;
+ }
+
+ @Override
+ public String toString() {
+ return "Name: " + name
+ + ", Amount: $" + amount
+ + ", Date: " + date
+ + ", Category: " + category;
+ }
+
+ /**
+ * Returns true if a given string is a valid expense name.
+ * @param name Name to be tested
+ */
+ public static boolean isValidName(String name) {
+ return name.matches(VALIDATION_REGEX);
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Expense expense = (Expense) o;
+ return Objects.equals(name, expense.name)
+ && Objects.equals(amount, expense.amount)
+ && Objects.equals(date, expense.date)
+ && Objects.equals(category, expense.category);
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ result = name != null ? name.hashCode() : 0;
+ temp = amount.hashCode();
+ result = 31 * result
+ + (int) (temp ^ (temp >>> 32));
+ result = 31 * result
+ + (date != null ? date.hashCode() : 0);
+ result = 31 * result
+ + (category != null ? category.hashCode() : 0);
+ return result;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setAmount(String amount) {
+ this.amount = new Price(amount);
+ }
+
+ public void setDate(LocalDate date) {
+ this.date = date;
+ }
+
+ public void setCategory(Category category) {
+ this.category = category;
+ }
+
+}
diff --git a/src/main/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicate.java b/src/main/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicate.java
new file mode 100644
index 00000000000..b8e3729cc98
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicate.java
@@ -0,0 +1,32 @@
+package fasttrack.model.expense;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import fasttrack.commons.util.StringUtil;
+
+
+/**
+ * Tests that a {@code Expense}'s {@code Name} matches any of the keywords given.
+ */
+public class ExpenseContainsKeywordsPredicate implements Predicate {
+ private final List keywords;
+
+ public ExpenseContainsKeywordsPredicate(List keywords) {
+ this.keywords = keywords;
+ }
+
+ @Override
+ public boolean test(Expense expense) {
+ return keywords.stream()
+ .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(expense.getName(), keyword));
+ }
+
+ @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/fasttrack/model/expense/ExpenseInCategoryPredicate.java b/src/main/java/fasttrack/model/expense/ExpenseInCategoryPredicate.java
new file mode 100644
index 00000000000..610763e44c6
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/ExpenseInCategoryPredicate.java
@@ -0,0 +1,37 @@
+package fasttrack.model.expense;
+
+import java.util.function.Predicate;
+
+import fasttrack.model.category.Category;
+
+/**
+ * Tests that a {@code Person}'s {@code Name} matches any of the keywords given.
+ */
+public class ExpenseInCategoryPredicate implements Predicate {
+ private final Category category;
+
+ public ExpenseInCategoryPredicate(Category category) {
+ this.category = category;
+ }
+
+ /**
+ * Get Category that this {@code ExpenseInCategoryPredicate}
+ * @return
+ */
+ public Category getCategory() {
+ return this.category;
+ }
+
+ @Override
+ public boolean test(Expense expense) {
+ return expense.getCategory().equals(this.category);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof ExpenseInCategoryPredicate // instanceof handles nulls
+ && category.equals(((ExpenseInCategoryPredicate) other).category)); // state check
+ }
+
+}
diff --git a/src/main/java/fasttrack/model/expense/ExpenseInTimespanPredicate.java b/src/main/java/fasttrack/model/expense/ExpenseInTimespanPredicate.java
new file mode 100644
index 00000000000..f8687eeab5e
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/ExpenseInTimespanPredicate.java
@@ -0,0 +1,54 @@
+package fasttrack.model.expense;
+
+import java.time.LocalDate;
+import java.util.function.Predicate;
+
+import fasttrack.logic.parser.ParserUtil;
+
+/**
+ * Tests that a {@code Person}'s {@code Name} matches any of the keywords given.
+ */
+public class ExpenseInTimespanPredicate implements Predicate {
+ private final ParserUtil.Timespan timespan;
+ private final LocalDate earliestDate;
+
+ /**
+ * Creates an {@code ExpenseInTimespanPredicate} which returns true if the given date is after the earliest date
+ * in the timespan.
+ * @param timespan Timespan of week, month or year
+ */
+ public ExpenseInTimespanPredicate(ParserUtil.Timespan timespan) {
+ this.timespan = timespan;
+ this.earliestDate = ParserUtil.getDateByTimespan(timespan);
+ }
+
+ /**
+ * Creates an {@code ExpenseInTimespanPredicate} which returns true if the given date is after {@code earliestDate}.
+ * @param earliestDate LocalDate of the earliestDate this predicate will return True for
+ */
+ public ExpenseInTimespanPredicate(LocalDate earliestDate) {
+ this.timespan = null;
+ this.earliestDate = earliestDate;
+ }
+
+ @Override
+ public boolean test(Expense expense) {
+ return expense.getDate().isAfter(earliestDate) || expense.getDate().isEqual(earliestDate);
+ }
+
+ /**
+ * Get Timespan that this {@code ExpenseInCategoryPredicate} is generated by.
+ * @return {@code Timespan} enum indicating week, month or year.
+ */
+ public ParserUtil.Timespan getTimespan() {
+ return this.timespan;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof ExpenseInTimespanPredicate // instanceof handles nulls
+ && earliestDate.equals(((ExpenseInTimespanPredicate) other).earliestDate)); // state check
+ }
+
+}
diff --git a/src/main/java/fasttrack/model/expense/ExpenseList.java b/src/main/java/fasttrack/model/expense/ExpenseList.java
new file mode 100644
index 00000000000..35f0627be67
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/ExpenseList.java
@@ -0,0 +1,158 @@
+package fasttrack.model.expense;
+
+import static fasttrack.commons.util.CollectionUtil.requireAllNonNull;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Iterator;
+import java.util.List;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.category.MiscellaneousCategory;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * A list of expenses that enforces uniqueness between its elements and
+ * does not allow nulls.
+ */
+public class ExpenseList implements Iterable {
+
+ private final ObservableList internalListOfExpenses = FXCollections.observableArrayList();
+ private final ObservableList internalUnmodifiableList = FXCollections
+ .unmodifiableObservableList(internalListOfExpenses);
+ private final Category misc = new MiscellaneousCategory();
+
+ /**
+ * Adds an expense to the internal list of expenses
+ * @param newExpense Expense to add
+ */
+ public void add(Expense newExpense) {
+ requireNonNull(newExpense);
+ internalListOfExpenses.add(newExpense);
+ }
+
+ /**
+ * Removes an expense from the internal list of expenses
+ * @param toRemove Expense to remove
+ */
+ public void remove(Expense toRemove) {
+ requireNonNull(toRemove);
+ internalListOfExpenses.remove(toRemove);
+ }
+
+ public void setExpense(Expense target, Expense editedExpense) {
+ requireAllNonNull(target, editedExpense);
+ int index = internalListOfExpenses.indexOf(target);
+
+ internalListOfExpenses.set(index, editedExpense);
+ }
+
+ public void set(int index, Expense newExpense) {
+ internalListOfExpenses.set(index, newExpense);
+ }
+
+ /**
+ * Replace expenses with {@code target} category with Misc object
+ * @param target
+ */
+ public void replaceDeletedCategory(Category target) {
+ requireNonNull(target);
+ internalListOfExpenses.forEach(expense -> {
+ if (expense.getCategory().equals(target)) {
+ expense.setCategory(misc);
+ }
+ });
+ }
+
+ /**
+ * Sets an internal list of expenses with a new list of expenses
+ * @param replacementList List of expenses to replace the current list
+ */
+ public void setExpenseList(ExpenseList replacementList) {
+ requireNonNull(replacementList);
+ internalListOfExpenses.setAll(replacementList.internalListOfExpenses);
+ }
+
+ /**
+ * Sets an internal list of expenses with a new list of expenses
+ * @param listOfExpenses List of expenses to replace the current list
+ */
+ public void setExpenseList(List listOfExpenses) {
+ requireAllNonNull(listOfExpenses);
+ internalListOfExpenses.setAll(listOfExpenses);
+ }
+
+ /**
+ * Returns the size of the internal list of expenses
+ * @return Size of the internal list of expenses
+ */
+ public int getSize() {
+ return internalListOfExpenses.size();
+ }
+
+ /**
+ * Returns the total amount of the internal list of expenses
+ * @return Total amount of the internal list of expenses
+ */
+ public double getTotalAmount() {
+ double totalAmount = 0;
+ for (Expense expense : internalListOfExpenses) {
+ totalAmount += expense.getAmount();
+ }
+ return totalAmount;
+ }
+
+ /**
+ * Delete all expense.
+ */
+ public void clear() {
+ internalListOfExpenses.clear();
+ internalUnmodifiableList.clear();
+ }
+
+ /**
+ * Sorts the internal list of expenses by date.
+ */
+ public void sortList() {
+ internalListOfExpenses.sort((o1, o2) -> o1.getDate().isAfter(o2.getDate()) ? -1
+ : o1.getDate().isEqual(o2.getDate()) ? 0 : 1);
+ }
+
+ /**
+ * Returns the backing list as an unmodifiable {@code ObservableList}.
+ */
+ public ObservableList asUnmodifiableList() {
+ return this.internalUnmodifiableList;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this.internalListOfExpenses.iterator();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ExpenseList)) {
+ return false;
+ }
+ ExpenseList otherInUniqueList = (ExpenseList) other;
+ return this.internalListOfExpenses.equals(otherInUniqueList.internalListOfExpenses);
+ }
+
+ /**
+ * Returns true if the list contains an equivalent person as the given argument.
+ * @param expense Expense to check
+ */
+ public boolean contains(Expense expense) {
+ requireNonNull(expense);
+ return internalListOfExpenses.stream().anyMatch(expense::equals);
+ }
+
+ @Override
+ public int hashCode() {
+ return internalListOfExpenses.hashCode();
+ }
+}
diff --git a/src/main/java/fasttrack/model/expense/Price.java b/src/main/java/fasttrack/model/expense/Price.java
new file mode 100644
index 00000000000..9eae0ed284e
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/Price.java
@@ -0,0 +1,87 @@
+package fasttrack.model.expense;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Objects;
+
+import fasttrack.logic.commands.exceptions.CommandException;
+
+
+
+/**
+ * Represents a Price of an Expense in FastTrack.
+ * Guarantees: immutable; is valid as declared in {@link #isValidPrice(String)}
+ */
+public class Price {
+
+ public static final String MESSAGE_CONSTRAINTS =
+ "Amounts should only contain numbers, and should not be negative or empty.";
+ public static final String VALIDATION_REGEX = "^(0|\\d*)(\\.\\d+)?$";
+ private String value;
+
+ /**
+ * Constructs a {@code Price}.
+ * @param price A valid price.
+ */
+ public Price(String price) {
+ requireNonNull(price);
+ if (!isValidPrice(price)) {
+ throw new IllegalArgumentException(MESSAGE_CONSTRAINTS);
+ }
+ value = price;
+ }
+ /**
+ * Constructs a {@code Price}.
+ * @param price A valid price.
+ */
+ public Price(double price) {
+ requireNonNull(price);
+ if (!(price > -0.0)) {
+ throw new IllegalArgumentException(MESSAGE_CONSTRAINTS);
+ }
+ value = String.valueOf(price);
+ }
+
+ /**
+ * Returns true if a given string is a valid price.
+ */
+ public static boolean isValidPrice(String test) {
+ return test.matches(VALIDATION_REGEX) && Double.parseDouble(test) > -0;
+ }
+
+
+ public double getPriceAsDouble() {
+ return Double.parseDouble(value);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ Price otherTypecasted = (Price) other;
+ return other == this // short circuit if same object
+ || (other instanceof Price // instanceof handles nulls
+ && Objects.equals(getPriceAsDouble(), otherTypecasted.getPriceAsDouble())); // state check
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String price) throws CommandException {
+ if (!isValidPrice(price)) {
+ throw new CommandException(MESSAGE_CONSTRAINTS);
+ }
+ value = price;
+ }
+
+}
+
diff --git a/src/main/java/fasttrack/model/expense/RecurringExpenseList.java b/src/main/java/fasttrack/model/expense/RecurringExpenseList.java
new file mode 100644
index 00000000000..090f4f206c5
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/RecurringExpenseList.java
@@ -0,0 +1,138 @@
+package fasttrack.model.expense;
+
+import static fasttrack.commons.util.CollectionUtil.requireAllNonNull;
+import static java.util.Objects.requireNonNull;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.category.MiscellaneousCategory;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * Represents a List of Recurring Expenses in the Expense Tracker.
+ */
+public class RecurringExpenseList {
+
+ private final ObservableList recurringExpenseList = FXCollections.observableArrayList();
+
+ private final ObservableList internalUnmodifiableList = FXCollections
+ .unmodifiableObservableList(recurringExpenseList);
+
+ private final Category misc = new MiscellaneousCategory();
+
+ /**
+ * Adds a recurring expense to the internal list of recurring expenses.
+ * @param recurringExpense Recurring expense to add.
+ */
+ public void addRecurringExpense(RecurringExpenseManager recurringExpense) {
+ recurringExpenseList.add(recurringExpense);
+ }
+
+
+ /**
+ * Removes a recurring expense from the internal list of recurring expenses.
+ * @param recurringExpense Recurring expense to remove.
+ */
+ public void removeRecurringExpense(RecurringExpenseManager recurringExpense) {
+ recurringExpenseList.remove(recurringExpense);
+ }
+
+
+ public ObservableList getRecurringExpenseList() {
+ return recurringExpenseList;
+ }
+
+
+ /**
+ * Returns the backing list as an unmodifiable {@code ObservableList}
+ * @return The unmodifiable list.
+ */
+ public ObservableList asUnmodifiableList() {
+ return this.internalUnmodifiableList;
+ }
+
+ public ArrayList getExpenses() {
+ ArrayList expenses = new ArrayList<>();
+ for (RecurringExpenseManager recurringExpense : recurringExpenseList) {
+ expenses.addAll(recurringExpense.getExpenses());
+ }
+ return expenses;
+ }
+
+ public void setRecurringExpenseList(RecurringExpenseList replacementList) {
+ recurringExpenseList.clear();
+ recurringExpenseList.addAll(replacementList.getRecurringExpenseList());
+ }
+
+ public void setRecurringExpenseList(List replacementList) {
+ requireAllNonNull(replacementList);
+ recurringExpenseList.setAll(replacementList);
+ }
+
+ /**
+ * Removes RecurringExpenseManager that are expired.
+ */
+ public void cleanupExpiredGenerators() {
+ recurringExpenseList.removeIf((generator) -> {
+ if (generator.getExpenseEndDate() == null) {
+ return false;
+ }
+ return LocalDate.now().isAfter(generator.getExpenseEndDate());
+ });
+ }
+
+
+ public int getSize() {
+ return recurringExpenseList.size();
+ }
+
+ /**
+ * Returns true if the list contains an equivalent recurring expense as the given argument.
+ * @param recurringExpense Recurring expense to be compared to
+ * @return Boolean depicting if the recurring expense is present in the list.
+ */
+ public boolean contains(RecurringExpenseManager recurringExpense) {
+ return recurringExpenseList.stream().anyMatch(recurringExpense::equals);
+ }
+
+ public double getTotalAmount() {
+ double totalAmount = 0;
+ for (RecurringExpenseManager recurringExpense : recurringExpenseList) {
+ totalAmount += recurringExpense.getTotalAmount();
+ }
+ return totalAmount;
+ }
+
+ /**
+ * Delete all recurring expense.
+ */
+ public void clear() {
+ recurringExpenseList.clear();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (RecurringExpenseManager recurringExpense : recurringExpenseList) {
+ sb.append(recurringExpense.toString());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Replace recurring expenses with {@code target} category with Misc object
+ * @param target
+ */
+ public void replaceDeletedCategory(Category target) {
+ requireNonNull(target);
+ recurringExpenseList.forEach(recurringExpenseManager -> {
+ if (recurringExpenseManager.getExpenseCategory().equals(target)) {
+ recurringExpenseManager.setExpenseCategory(misc);
+ }
+ });
+ }
+}
diff --git a/src/main/java/fasttrack/model/expense/RecurringExpenseManager.java b/src/main/java/fasttrack/model/expense/RecurringExpenseManager.java
new file mode 100644
index 00000000000..9697b03f36a
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/RecurringExpenseManager.java
@@ -0,0 +1,201 @@
+package fasttrack.model.expense;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Objects;
+
+import fasttrack.model.category.Category;
+
+/**
+ * Represents a Recurring Expense in the Expense Tracker.
+ */
+public class RecurringExpenseManager {
+ private String expenseName;
+ private Price amount;
+ private Category expenseCategory;
+ private int numberOfExpenses = 0;
+ private LocalDate nextExpenseDate = null;
+ private LocalDate startDate;
+ private LocalDate endDate = null;
+ private RecurringExpenseType recurringExpenseType;
+
+ /**
+ * The constructor for the RecurringExpenseManager class with a start and end
+ * date.
+ * @param expenseName The name of the recurring expense.
+ * @param expenseAmount The amount of the recurring expense.
+ * @param expenseCategory The category of the recurring expense.
+ * @param startDate The start date of the recurring expense.
+ * @param endDate The end date of the recurring expense.
+ * @param recurringExpenseType The type of the recurring expense.
+ */
+ public RecurringExpenseManager(String expenseName, Price expenseAmount,
+ Category expenseCategory, LocalDate startDate, LocalDate endDate,
+ RecurringExpenseType recurringExpenseType) {
+ this.expenseName = expenseName;
+ this.amount = expenseAmount;
+ this.expenseCategory = expenseCategory;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.recurringExpenseType = recurringExpenseType;
+ this.nextExpenseDate = startDate;
+ }
+
+ /**
+ * The constructor for the RecurringExpenseManager class with a start and end
+ * date.
+ * @param expenseName The name of the recurring expense.
+ * @param expenseAmount The amount of the recurring expense.
+ * @param expenseCategory The category of the recurring expense.
+ * @param startDate The start date of the recurring expense.
+ * @param endDate The end date of the recurring expense.
+ * @param recurringExpenseType The type of the recurring expense.
+ */
+ public RecurringExpenseManager(String expenseName, double expenseAmount,
+ Category expenseCategory, LocalDate startDate, LocalDate endDate,
+ RecurringExpenseType recurringExpenseType) {
+ this.expenseName = expenseName;
+ this.amount = new Price(expenseAmount);
+ this.expenseCategory = expenseCategory;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.recurringExpenseType = recurringExpenseType;
+ this.nextExpenseDate = startDate;
+ }
+
+ /**
+ * The constructor for the RecurringExpenseManager class with no end date.
+ * @param expenseName The name of the recurring expense.
+ * @param amount The amount of the recurring expense.
+ * @param expenseCategory The category of the recurring expense.
+ * @param startDate The start date of the recurring expense.
+ * @param recurringExpenseType The type of the recurring expense.
+ */
+ public RecurringExpenseManager(String expenseName, Price amount,
+ Category expenseCategory, LocalDate startDate, RecurringExpenseType recurringExpenseType) {
+ this.expenseName = expenseName;
+ this.amount = amount;
+ this.expenseCategory = expenseCategory;
+ this.startDate = startDate;
+ this.recurringExpenseType = recurringExpenseType;
+ this.nextExpenseDate = startDate;
+ }
+
+ /**
+ * The constructor for the RecurringExpenseManager class with no end date.
+ * @param expenseName The name of the recurring expense.
+ * @param amount The amount of the recurring expense.
+ * @param expenseCategory The category of the recurring expense.
+ * @param startDate The start date of the recurring expense.
+ * @param recurringExpenseType The type of the recurring expense.
+ */
+ public RecurringExpenseManager(String expenseName, double amount,
+ Category expenseCategory, LocalDate startDate, RecurringExpenseType recurringExpenseType) {
+ this.expenseName = expenseName;
+ this.amount = new Price(amount);
+ this.expenseCategory = expenseCategory;
+ this.startDate = startDate;
+ this.recurringExpenseType = recurringExpenseType;
+ this.nextExpenseDate = startDate;
+ }
+
+ public ArrayList getExpenses() {
+ ArrayList expenses = new ArrayList<>();
+ LocalDate newEndDate = LocalDate.now();
+ if (endDate != null) {
+ newEndDate = !endDate.isAfter(LocalDate.now()) ? endDate : LocalDate.now();
+ }
+ while (!nextExpenseDate.isAfter(newEndDate)) {
+ expenses.add(new Expense(expenseName, amount, nextExpenseDate, expenseCategory));
+ nextExpenseDate = recurringExpenseType.getNextExpenseDate(nextExpenseDate);
+ }
+ numberOfExpenses = expenses.size();
+ return expenses;
+ }
+
+ public void setExpenseCategory(Category expenseCategory) {
+ this.expenseCategory = expenseCategory;
+ }
+
+ public void setExpenseName(String expenseName) {
+ this.expenseName = expenseName;
+ }
+
+ public void setAmount(String expenseAmount) {
+ this.amount = new Price(expenseAmount);
+ }
+
+ public void setAmount(double expenseAmount) {
+ this.amount = new Price(expenseAmount);
+ }
+
+ public void setEndDate(LocalDate endDate) {
+ this.endDate = endDate;
+ }
+
+ public void setRecurringExpenseType(RecurringExpenseType recurringExpenseType) {
+ this.recurringExpenseType = recurringExpenseType;
+ }
+
+ public int getNumberOfExpenses() {
+ return numberOfExpenses;
+ }
+
+ public double getTotalAmount() {
+ return amount.getPriceAsDouble() * numberOfExpenses;
+ }
+
+ public LocalDate getNextExpenseDate() {
+ return nextExpenseDate;
+ }
+
+ public LocalDate getExpenseStartDate() {
+ return startDate;
+ }
+
+ public RecurringExpenseType getRecurringExpenseType() {
+ return recurringExpenseType;
+ }
+
+ public LocalDate getExpenseEndDate() {
+ return endDate;
+ }
+
+ public String getExpenseName() {
+ return expenseName;
+ }
+
+ public Category getExpenseCategory() {
+ return expenseCategory;
+ }
+
+ public double getAmount() {
+ return amount.getPriceAsDouble();
+ }
+
+ public void setNextExpenseDate(LocalDate nextExpenseDate) {
+ this.nextExpenseDate = nextExpenseDate;
+ }
+
+ @Override
+ public String toString() {
+ String endStatus = endDate == null ? "Ongoing" : String.valueOf(endDate);
+ return "Recurring Expense: " + expenseName + ", Amount: " + amount + ", Category: "
+ + expenseCategory + ", Start Date: " + startDate + ", End Date: " + endStatus
+ + ", Recurring Expense Type: " + recurringExpenseType;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof RecurringExpenseManager) {
+ RecurringExpenseManager other = (RecurringExpenseManager) obj;
+ return this.expenseName.equals(other.expenseName)
+ && this.amount.equals(other.amount)
+ && this.expenseCategory.equals(other.expenseCategory)
+ && this.startDate.equals(other.startDate)
+ && Objects.equals(endDate, other.endDate)
+ && this.recurringExpenseType.equals(other.recurringExpenseType);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/fasttrack/model/expense/RecurringExpenseType.java b/src/main/java/fasttrack/model/expense/RecurringExpenseType.java
new file mode 100644
index 00000000000..0e1109dd5ff
--- /dev/null
+++ b/src/main/java/fasttrack/model/expense/RecurringExpenseType.java
@@ -0,0 +1,37 @@
+package fasttrack.model.expense;
+
+import java.time.LocalDate;
+/**
+ * Enum for Recurring Expense Type.
+ */
+public enum RecurringExpenseType {
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ YEARLY;
+
+ /**
+ * Returns the next expense date based on the recurring expense type.
+ * @param currentDate The current date.
+ * @return The next expense date.
+ */
+ public LocalDate getNextExpenseDate(LocalDate currentDate) {
+ switch (this) {
+ case MONTHLY:
+ currentDate = currentDate.plusMonths(1);
+ break;
+ case WEEKLY:
+ currentDate = currentDate.plusWeeks(1);
+ break;
+ case DAILY:
+ currentDate = currentDate.plusDays(1);
+ break;
+ case YEARLY:
+ currentDate = currentDate.plusYears(1);
+ break;
+ default:
+ break;
+ }
+ return currentDate;
+ }
+}
diff --git a/src/main/java/fasttrack/model/util/AnalyticsType.java b/src/main/java/fasttrack/model/util/AnalyticsType.java
new file mode 100644
index 00000000000..87601844f7d
--- /dev/null
+++ b/src/main/java/fasttrack/model/util/AnalyticsType.java
@@ -0,0 +1,23 @@
+package fasttrack.model.util;
+
+/**
+ * Represents different types of analytics that can be calculated in FastTrack
+ * MONTHLY_SPENT: Represents the total amount spent in a month.
+ * MONTHLY_REMAINING: Represents the remaining budget for the month.
+ * WEEKLY_SPENT: Represents the total amount spent in a week.
+ * WEEKLY_REMAINING: Represents the remaining budget for the week.
+ * WEEKLY_CHANGE: Represents the change in spending from the previous week.
+ * MONTHLY_CHANGE: Represents the change in spending from the previous month.
+ * TOTAL_SPENT: Represents the total amount spent overall.
+ * BUDGET_PERCENTAGE: Represents the percentage of the budget that has been spent.
+ */
+public enum AnalyticsType {
+ MONTHLY_SPENT,
+ MONTHLY_REMAINING,
+ WEEKLY_SPENT,
+ WEEKLY_REMAINING,
+ WEEKLY_CHANGE,
+ MONTHLY_CHANGE,
+ TOTAL_SPENT,
+ BUDGET_PERCENTAGE
+}
diff --git a/src/main/java/fasttrack/model/util/CommandUtility.java b/src/main/java/fasttrack/model/util/CommandUtility.java
new file mode 100644
index 00000000000..35d31b2f6d1
--- /dev/null
+++ b/src/main/java/fasttrack/model/util/CommandUtility.java
@@ -0,0 +1,40 @@
+package fasttrack.model.util;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+/**
+ * Utility class which contains helper functions related to various commands, such as conversion of date and times
+ */
+public class CommandUtility {
+
+ /**
+ * Parses a date string input in any allowed format and returns a LocalDate object.
+ * @param input A date string of any allowed format
+ * @return A LocalDate object representing the parsed date
+ * @throws IllegalArgumentException If the input string cannot be parsed into a valid date
+ */
+ public static LocalDate parseDateFromUserInput(String input) throws IllegalArgumentException {
+ DateTimeFormatter[] formatters = {
+ DateTimeFormatter.ofPattern("dd/MM/yy"),
+ DateTimeFormatter.ofPattern("dd/MM/yyyy"),
+ DateTimeFormatter.ofPattern("d/M/yy"),
+ DateTimeFormatter.ofPattern("d/M/yyyy"),
+ DateTimeFormatter.ofPattern("d/MM/yy"),
+ DateTimeFormatter.ofPattern("d/MM/yyyy"),
+ DateTimeFormatter.ofPattern("dd/M/yy"),
+ DateTimeFormatter.ofPattern("dd/M/yyyy")
+ };
+ for (DateTimeFormatter formatter : formatters) {
+ try {
+ return LocalDate.parse(input, formatter);
+ } catch (DateTimeParseException ignored) {
+ continue;
+ }
+ }
+ throw new IllegalArgumentException("Invalid date format");
+ }
+
+}
+
diff --git a/src/main/java/fasttrack/model/util/SampleExpenseTracker.java b/src/main/java/fasttrack/model/util/SampleExpenseTracker.java
new file mode 100644
index 00000000000..a72a85c4d18
--- /dev/null
+++ b/src/main/java/fasttrack/model/util/SampleExpenseTracker.java
@@ -0,0 +1,66 @@
+package fasttrack.model.util;
+
+import java.time.LocalDate;
+
+import fasttrack.model.Budget;
+import fasttrack.model.ExpenseTracker;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.category.Category;
+import fasttrack.model.category.UserDefinedCategory;
+import fasttrack.model.expense.Expense;
+
+/**
+ * Class containing sample data for ExpenseTracker to initialize if needed.
+ */
+public class SampleExpenseTracker {
+
+ private static final Category food = new UserDefinedCategory("Food", "For food");
+ private static final Category entertainment = new UserDefinedCategory("Entertainment", "For entertainment");
+ private static final Category transportation = new UserDefinedCategory("Transportation", "For bus, car, train");
+ private static final Category shopping = new UserDefinedCategory("Shopping", "");
+ private static final Category housing = new UserDefinedCategory("Housing", "");
+ /**
+ * Sample data for categories
+ * @return an array of sample categories.
+ */
+ public static Category[] getSampleCategories() {
+ return new Category[] {
+ food, entertainment, transportation, shopping, housing
+ };
+ }
+
+ /**
+ * Sample data for expenses
+ * @return an array of sample expenses.
+ */
+ public static Expense[] getSampleExpenses() {
+ return new Expense[] {
+ new Expense("Meal at JE", "4.50", LocalDate.now(), food),
+ new Expense("Movie ticket", "12.99", LocalDate.of(2023, 3, 15), entertainment),
+ new Expense("MRT fare", "45.80", LocalDate.of(2023, 3, 10), transportation),
+ new Expense("Shoes", "75.00", LocalDate.of(2023, 3, 20), shopping),
+ new Expense("Groceries", "56.30", LocalDate.of(2023, 3, 25), food)
+ };
+ }
+
+ /**
+ * Sets all required sample data for categories and expenses.
+ * @return ReadOnlyExpenseTracker to be read from.
+ */
+ public static ReadOnlyExpenseTracker getSampleExpenseTracker() {
+ ExpenseTracker sampleExpenseTracker = new ExpenseTracker();
+ for (Category sampleCategory : getSampleCategories()) {
+ sampleExpenseTracker.addCategory(sampleCategory);
+ }
+
+ for (Expense sampleExpense : getSampleExpenses()) {
+ if (sampleExpenseTracker.getCategoryInstance(sampleExpense.getCategory()) != null) {
+ sampleExpense.setCategory(sampleExpenseTracker.getCategoryInstance(sampleExpense.getCategory()));
+ }
+ sampleExpenseTracker.addExpense(sampleExpense);
+ }
+
+ sampleExpenseTracker.setBudget(new Budget(0));
+ return sampleExpenseTracker;
+ }
+}
diff --git a/src/main/java/fasttrack/model/util/StorageUtility.java b/src/main/java/fasttrack/model/util/StorageUtility.java
new file mode 100644
index 00000000000..31e4ad28b74
--- /dev/null
+++ b/src/main/java/fasttrack/model/util/StorageUtility.java
@@ -0,0 +1,22 @@
+package fasttrack.model.util;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+
+/**
+ * Utility class which contains helper functions related to various commands, such as conversion of date and times
+ */
+public class StorageUtility {
+
+ /**
+ * Parses a date string in the format of "yyyy-MM-dd" from JSON to a LocalDate object.
+ * @param dateString the date string to parse
+ * @return a LocalDate object representing the parsed date
+ */
+ public static LocalDate parseDateFromJson(String dateString) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ return LocalDate.parse(dateString, formatter);
+ }
+}
+
diff --git a/src/main/java/fasttrack/model/util/UserInterfaceUtil.java b/src/main/java/fasttrack/model/util/UserInterfaceUtil.java
new file mode 100644
index 00000000000..d8b29e95b24
--- /dev/null
+++ b/src/main/java/fasttrack/model/util/UserInterfaceUtil.java
@@ -0,0 +1,45 @@
+package fasttrack.model.util;
+
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Utility class which contains functions for utility purposes, such as conversion of dates and prices
+ * @author Nicholas Lee
+ */
+public class UserInterfaceUtil {
+
+ /**
+ * Returns a string representation of the given date object in the format "dd/MM/yy".
+ * @param date the date object to be formatted
+ * @return a formatted date string
+ */
+ public static String parseDate(LocalDate date) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yy");
+ return date.format(formatter);
+ }
+
+ /**
+ * Returns a string representation of the given double as a price string in the format "$xx.xx".
+ * @param amount double value representing a price
+ * @return a formatted price string representation
+ */
+ public static String parsePrice(double amount) {
+ DecimalFormat df = new DecimalFormat("#0.00");
+ String priceString = df.format(amount);
+ return "$" + priceString;
+ }
+
+
+ /**
+ * Returns the input string with the first letter capitalized and the rest of the letters in lower case.
+ * @param input the string to be capitalized
+ * @return a string with the first letter capitalized
+ */
+ public static String capitalizeFirstLetter(final String input) {
+ return Character.toUpperCase(input.charAt(0)) + input.substring(1);
+ }
+}
+
+
diff --git a/src/main/java/fasttrack/storage/ExpenseTrackerStorage.java b/src/main/java/fasttrack/storage/ExpenseTrackerStorage.java
new file mode 100644
index 00000000000..50a6906c0f5
--- /dev/null
+++ b/src/main/java/fasttrack/storage/ExpenseTrackerStorage.java
@@ -0,0 +1,45 @@
+package fasttrack.storage;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.model.ReadOnlyExpenseTracker;
+
+/**
+ * Represents a storage for {@link fasttrack.model.ExpenseTracker}.
+ */
+public interface ExpenseTrackerStorage {
+
+ /**
+ * Returns the file path of the data file.
+ */
+ Path getExpenseTrackerFilePath();
+
+ /**
+ * Returns ExpenseTracker data as a {@link ReadOnlyExpenseTracker}.
+ * 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 readExpenseTracker() throws DataConversionException, IOException;
+
+ /**
+ * @see #getExpenseTrackerFilePath()
+ */
+ Optional readExpenseTracker(Path filePath) throws DataConversionException, IOException;
+
+ /**
+ * Saves the given {@link ReadOnlyExpenseTracker} to the storage.
+ * @param expenseTracker cannot be null.
+ * @throws IOException if there was any problem writing to the file.
+ */
+ void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException;
+
+ /**
+ * @see #saveExpenseTracker(ReadOnlyExpenseTracker)
+ */
+ void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, Path filePath) throws IOException;
+
+}
diff --git a/src/main/java/fasttrack/storage/JsonAdaptedBudget.java b/src/main/java/fasttrack/storage/JsonAdaptedBudget.java
new file mode 100644
index 00000000000..c552e66b856
--- /dev/null
+++ b/src/main/java/fasttrack/storage/JsonAdaptedBudget.java
@@ -0,0 +1,48 @@
+package fasttrack.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import fasttrack.commons.exceptions.IllegalValueException;
+import fasttrack.model.Budget;
+
+
+/**
+ * Jackson-friendly version of {@link Budget}.
+ */
+class JsonAdaptedBudget {
+
+ public static final String MISSING_FIELD_MESSAGE_FORMAT = "Budget's %s field is missing!";
+ private final String amount;
+
+ /**
+ * Constructs a {@code JsonAdaptedBudget} with the given budget details.
+ */
+ @JsonCreator
+ public JsonAdaptedBudget(@JsonProperty("amount") String amount) {
+ this.amount = amount;
+ }
+
+ /**
+ * Converts a given {@code Budget} into this class for Jackson use.
+ */
+ public JsonAdaptedBudget(Budget source) {
+ this.amount = source.toString();
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted budget object into the model's
+ * {@code Budget} object.
+ * @throws IllegalValueException if there were any data constraints violated
+ * in the adapted budget.
+ */
+ public Budget toModelType() throws IllegalValueException {
+
+ if (amount == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT));
+ }
+
+ return new Budget(Double.parseDouble(amount));
+ }
+
+}
diff --git a/src/main/java/fasttrack/storage/JsonAdaptedCategory.java b/src/main/java/fasttrack/storage/JsonAdaptedCategory.java
new file mode 100644
index 00000000000..e59303f483e
--- /dev/null
+++ b/src/main/java/fasttrack/storage/JsonAdaptedCategory.java
@@ -0,0 +1,65 @@
+package fasttrack.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import fasttrack.commons.exceptions.IllegalValueException;
+import fasttrack.model.category.Category;
+import fasttrack.model.category.MiscellaneousCategory;
+import fasttrack.model.category.UserDefinedCategory;
+
+
+
+/**
+ * Jackson-friendly version of {@link Category}.
+ */
+class JsonAdaptedCategory {
+
+ public static final String MISSING_FIELD_MESSAGE_FORMAT = "Category's %s field is missing!";
+ private final String categoryName;
+ private final String summary;
+
+ /**
+ * Constructs a {@code JsonAdaptedCategory} with the given category details.
+ */
+ @JsonCreator
+ public JsonAdaptedCategory(@JsonProperty("categoryName") String categoryName,
+ @JsonProperty("summary") String summary) {
+ this.categoryName = categoryName;
+ this.summary = summary;
+ }
+
+ /**
+ * Converts a given {@code Category} into this class for Jackson use.
+ */
+ public JsonAdaptedCategory(Category source) {
+ this.categoryName = source.getCategoryName();
+ this.summary = source.getSummary();
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted category object into the model's
+ * {@code Category} object.
+ * @throws IllegalValueException if there were any data constraints violated
+ * in the adapted category.
+ */
+ public Category toModelType() throws IllegalValueException {
+
+ if (categoryName == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Category"));
+ }
+
+ final String modelCategoryName = categoryName;
+ if (summary == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Summary"));
+ }
+
+ final String modelDescription = summary;
+
+ if (categoryName.equalsIgnoreCase("misc")) {
+ return new MiscellaneousCategory();
+ }
+ return new UserDefinedCategory(modelCategoryName, modelDescription);
+ }
+
+}
diff --git a/src/main/java/fasttrack/storage/JsonAdaptedExpense.java b/src/main/java/fasttrack/storage/JsonAdaptedExpense.java
new file mode 100644
index 00000000000..a574bf05f5d
--- /dev/null
+++ b/src/main/java/fasttrack/storage/JsonAdaptedExpense.java
@@ -0,0 +1,81 @@
+package fasttrack.storage;
+
+import java.time.LocalDate;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import fasttrack.commons.exceptions.IllegalValueException;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.util.StorageUtility;
+
+/**
+ * Jackson-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 amount;
+ private final String date;
+ private final JsonAdaptedCategory category;
+
+
+
+ /**
+ * Constructs a {@code JsonAdaptedExpense} with the given expense details.
+ * @param name Name of the expense.
+ * @param amount Amount of the expense.
+ * @param date Date of the expense.
+ * @param category Category of the expense.
+ */
+ @JsonCreator
+ public JsonAdaptedExpense(@JsonProperty("name") String name, @JsonProperty("amount") String amount,
+ @JsonProperty("date") String date,
+ @JsonProperty("category") JsonAdaptedCategory category) {
+ this.name = name;
+ this.amount = amount;
+ this.category = category;
+ this.date = date;
+ }
+
+ /**
+ * Converts a given {@code Expense} into this class for Jackson use.
+ * https://stackoverflow.com/questions/530012/how-to-convert-java-util-date-to-java-sql-date
+ * @param source future changes to this will not affect the created {@code JsonAdaptedExpense}.
+ */
+ public JsonAdaptedExpense(Expense source) {
+ name = source.getName();
+ amount = Double.toString(source.getAmount());
+ date = source.getFormattedDate();
+ category = new JsonAdaptedCategory(
+ source.getCategory().getCategoryName(), source.getCategory().getSummary());
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted expense object into the model's {@code Expense} object.
+ * @throws IllegalValueException if there were any data constraints violated in the adapted expense.
+ */
+ public Expense toModelType() throws IllegalValueException {
+ if (name == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Name"));
+ }
+ if (!Expense.isValidName(name)) {
+ throw new IllegalValueException(Expense.MESSAGE_CONSTRAINTS);
+ }
+
+ final String modelName = name;
+
+ if (date == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Date"));
+ }
+
+ final LocalDate modelDate = StorageUtility.parseDateFromJson(date);
+
+ Category modelCategory = category.toModelType();
+
+ return new Expense(modelName, amount, modelDate, modelCategory);
+ }
+}
diff --git a/src/main/java/fasttrack/storage/JsonAdaptedRecurringExpenseManager.java b/src/main/java/fasttrack/storage/JsonAdaptedRecurringExpenseManager.java
new file mode 100644
index 00000000000..8cd73cf633a
--- /dev/null
+++ b/src/main/java/fasttrack/storage/JsonAdaptedRecurringExpenseManager.java
@@ -0,0 +1,127 @@
+package fasttrack.storage;
+
+import java.time.LocalDate;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import fasttrack.commons.exceptions.IllegalValueException;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Price;
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.model.expense.RecurringExpenseType;
+import fasttrack.model.util.StorageUtility;
+
+/**
+ * Jackson-friendly version of {@link RecurringExpenseManager}.
+ */
+public class JsonAdaptedRecurringExpenseManager {
+ public static final String MISSING_FIELD_MESSAGE_FORMAT = "Recurring Expense Manager's field is missing!";
+
+ private final String expenseName;
+ private final String expenseAmount;
+ private final JsonAdaptedCategory expenseCategory;
+ private final String nextExpenseDate;
+ private final String startDate;
+ private final String endDate;
+ private final String recurringExpenseType;
+
+ /**
+ * Constructs a {@code JsonAdaptedRecurringExpenseManager} with the given
+ * category details.
+ * @param expenseName Name of the expense.
+ * @param expenseAmount Amount of the expense.
+ * @param expenseCategory Category of the expense.
+ * @param nextExpenseDate The next date at which the expense will be
+ * charged.
+ * @param startDate The starting date at which the recurring expense
+ * was first added.
+ * @param endDate The ending date at which the recurring expense
+ * will end.
+ * @param recurringExpenseType Frequency-interval of which the expense will be
+ * added.
+ */
+ @JsonCreator
+ public JsonAdaptedRecurringExpenseManager(@JsonProperty("expenseName") String expenseName,
+ @JsonProperty("expenseAmount") String expenseAmount,
+ @JsonProperty("expenseCategory") JsonAdaptedCategory expenseCategory,
+ @JsonProperty("nextExpenseDate") String nextExpenseDate,
+ @JsonProperty("startDate") String startDate,
+ @JsonProperty("endDate") String endDate,
+ @JsonProperty("recurringExpenseType") String recurringExpenseType) {
+ this.expenseName = expenseName;
+ this.expenseAmount = expenseAmount;
+ this.expenseCategory = expenseCategory;
+ this.nextExpenseDate = nextExpenseDate;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.recurringExpenseType = recurringExpenseType;
+ }
+
+ /**
+ * Converts a given {@code RecurringExpenseManager} into this class for Jackson
+ * use.
+ * @param source future changes to this will not affect the created
+ * {@code JsonAdaptedRecurringExpenseManager}
+ */
+ public JsonAdaptedRecurringExpenseManager(RecurringExpenseManager source) {
+ this.expenseName = source.getExpenseName();
+ this.expenseAmount = Double.toString(source.getAmount());
+ this.expenseCategory = new JsonAdaptedCategory(source.getExpenseCategory().getCategoryName(),
+ source.getExpenseCategory().getSummary());
+ this.nextExpenseDate = String.valueOf(source.getNextExpenseDate());
+ this.startDate = String.valueOf(source.getExpenseStartDate());
+ this.endDate = String.valueOf(source.getExpenseEndDate());
+ this.recurringExpenseType = String.valueOf(source.getRecurringExpenseType());
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted RecurringExpenseManager object into
+ * the model's
+ * {@code RecurringExpenseManager} object.
+ * @throws IllegalValueException if there were any data constraints violated
+ * in the adapted category.
+ */
+ public RecurringExpenseManager toModelType() throws IllegalValueException {
+ if (expenseName == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT));
+ }
+
+ if (expenseAmount == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT));
+ }
+
+ if (expenseCategory == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT));
+ }
+
+ if (startDate == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT));
+ }
+
+ if (recurringExpenseType == null) {
+ throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT));
+ }
+
+ LocalDate modelStartDate = StorageUtility.parseDateFromJson(startDate);
+
+ LocalDate modelNextExpenseDate = StorageUtility.parseDateFromJson(nextExpenseDate);
+
+ RecurringExpenseType modelRecurringType = RecurringExpenseType.valueOf(recurringExpenseType);
+
+ Category toBeUsed = expenseCategory.toModelType();
+ Price amount = new Price(expenseAmount);
+ if (!endDate.equals("null")) {
+ LocalDate modelEndDate = StorageUtility.parseDateFromJson(endDate);
+ RecurringExpenseManager toReturn = new RecurringExpenseManager(expenseName, amount,
+ toBeUsed, modelStartDate, modelEndDate, modelRecurringType);
+ toReturn.setNextExpenseDate(modelNextExpenseDate);
+ return toReturn;
+ }
+
+ RecurringExpenseManager toReturn = new RecurringExpenseManager(expenseName, amount,
+ toBeUsed, modelStartDate, modelRecurringType);
+ toReturn.setNextExpenseDate(modelNextExpenseDate);
+ return toReturn;
+ }
+}
diff --git a/src/main/java/fasttrack/storage/JsonExpenseTrackerStorage.java b/src/main/java/fasttrack/storage/JsonExpenseTrackerStorage.java
new file mode 100644
index 00000000000..881d314fe8b
--- /dev/null
+++ b/src/main/java/fasttrack/storage/JsonExpenseTrackerStorage.java
@@ -0,0 +1,76 @@
+package fasttrack.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 fasttrack.commons.core.LogsCenter;
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.commons.exceptions.IllegalValueException;
+import fasttrack.commons.util.FileUtil;
+import fasttrack.commons.util.JsonUtil;
+import fasttrack.model.ReadOnlyExpenseTracker;
+
+/**
+ * A class to access ExpenseTracker data stored as a json file on the hard disk.
+ */
+public class JsonExpenseTrackerStorage implements ExpenseTrackerStorage {
+
+ private static final Logger logger = LogsCenter.getLogger(JsonExpenseTrackerStorage.class);
+ private Path filePath;
+
+ public JsonExpenseTrackerStorage(Path filePath) {
+ this.filePath = filePath;
+ }
+
+ public Path getExpenseTrackerFilePath() {
+ return filePath;
+ }
+
+ @Override
+ public Optional readExpenseTracker() throws DataConversionException {
+ return readExpenseTracker(filePath);
+ }
+
+ /**
+ * Similar to {@link #readExpenseTracker()}.
+ * @param filePath location of the data. Cannot be null.
+ * @throws DataConversionException if the file is not in the correct format.
+ */
+ public Optional readExpenseTracker(Path filePath) throws DataConversionException {
+ requireNonNull(filePath);
+
+ Optional jsonExpenseTracker = JsonUtil.readJsonFile(
+ filePath, JsonSerializableExpenseTracker.class);
+ if (!jsonExpenseTracker.isPresent()) {
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(jsonExpenseTracker.get().toModelType());
+ } catch (IllegalValueException ive) {
+ logger.info("Illegal values found in " + filePath + ": " + ive.getMessage());
+ throw new DataConversionException(ive);
+ }
+ }
+
+ @Override
+ public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException {
+ saveExpenseTracker(expenseTracker, filePath);
+ }
+
+ /**
+ * Similar to {@link #saveExpenseTracker(ReadOnlyExpenseTracker)}.
+ * @param filePath location of the data. Cannot be null.
+ */
+ public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, Path filePath) throws IOException {
+ requireNonNull(expenseTracker);
+ requireNonNull(filePath);
+ FileUtil.createIfMissing(filePath);
+ JsonUtil.saveJsonFile(new JsonSerializableExpenseTracker(expenseTracker), filePath);
+ }
+
+}
diff --git a/src/main/java/fasttrack/storage/JsonSerializableExpenseTracker.java b/src/main/java/fasttrack/storage/JsonSerializableExpenseTracker.java
new file mode 100644
index 00000000000..0ccf0245f61
--- /dev/null
+++ b/src/main/java/fasttrack/storage/JsonSerializableExpenseTracker.java
@@ -0,0 +1,113 @@
+package fasttrack.storage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonRootName;
+
+import fasttrack.commons.exceptions.IllegalValueException;
+import fasttrack.model.ExpenseTracker;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.category.Category;
+import fasttrack.model.category.MiscellaneousCategory;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+
+/**
+ * An Immutable ExpenseTracker that is serializable to JSON format.
+ */
+@JsonRootName(value = "expenseTracker")
+class JsonSerializableExpenseTracker {
+
+ private final List categories = new ArrayList<>();
+ private final List expenses = new ArrayList<>();
+ private final List recurringGenerators = new ArrayList<>();
+ private final JsonAdaptedBudget budget;
+
+ /**
+ * Constructs a {@code JsonSerializableExpenseTracker} with the given expenses
+ * and categories.
+ * @param listOfCategories list of categories to be added to the ExpenseTracker
+ * @param listOfExpenses list of expenses to be added to the ExpenseTracker
+ */
+ @JsonCreator
+ public JsonSerializableExpenseTracker(@JsonProperty("categories") List listOfCategories,
+ @JsonProperty("expenses") List listOfExpenses,
+ @JsonProperty("budget") JsonAdaptedBudget budget,
+ @JsonProperty("recurringGenerators") List recurringGenerators) {
+ this.categories.addAll(listOfCategories);
+ this.expenses.addAll(listOfExpenses);
+ this.budget = budget;
+ this.recurringGenerators.addAll(recurringGenerators);
+ }
+
+ /**
+ * Converts a given {@code ReadOnlyExpenseTracker} into this class for Jackson
+ * use.
+ * @param source future changes to this will not affect the created
+ * {@code JsonSerializableExpenseTracker}.
+ */
+ public JsonSerializableExpenseTracker(ReadOnlyExpenseTracker source) {
+ this.categories.addAll(source.getCategoryList()
+ .stream().map(JsonAdaptedCategory::new).collect(Collectors.toList()));
+ this.expenses.addAll(source.getExpenseList()
+ .stream().map(JsonAdaptedExpense::new).collect(Collectors.toList()));
+ this.budget = new JsonAdaptedBudget(source.getBudget());
+ this.recurringGenerators.addAll(source.getRecurringExpenseGenerators()
+ .stream().map(JsonAdaptedRecurringExpenseManager::new).collect(Collectors.toList()));
+ }
+
+ /**
+ * Converts this ExpenseTracker into the model's {@code ExpenseTracker} object.
+ * @throws IllegalValueException if there were any data constraints violated.
+ */
+ public ExpenseTracker toModelType() throws IllegalValueException {
+ ExpenseTracker expenseTracker = new ExpenseTracker();
+
+ for (JsonAdaptedCategory jsonAdaptedCategory : categories) {
+ Category category = jsonAdaptedCategory.toModelType();
+ expenseTracker.addCategory(category);
+ }
+
+ for (JsonAdaptedRecurringExpenseManager jsonAdaptedGenerator : recurringGenerators) {
+ RecurringExpenseManager expenseGenerator = jsonAdaptedGenerator.toModelType();
+ Category associatedCategory = getAssociatedCategoryForRecurring(expenseGenerator, expenseTracker);
+ if (associatedCategory == null) {
+ if (!(expenseGenerator.getExpenseCategory() instanceof MiscellaneousCategory)) {
+ expenseTracker.addCategory(expenseGenerator.getExpenseCategory());
+ }
+ } else {
+ expenseGenerator.setExpenseCategory(associatedCategory);
+ }
+ expenseTracker.addRecurringGenerator(expenseGenerator);
+ }
+
+ for (JsonAdaptedExpense jsonAdaptedExpense : expenses) {
+ Expense expense = jsonAdaptedExpense.toModelType();
+ Category associatedCategory = getAssociatedCategory(expense, expenseTracker);
+ if (associatedCategory == null) {
+ if (!(expense.getCategory() instanceof MiscellaneousCategory)) {
+ expenseTracker.addCategory(expense.getCategory());
+ }
+ } else {
+ expense.setCategory(associatedCategory);
+ }
+ expenseTracker.addExpense(expense);
+ }
+
+ expenseTracker.setBudget(budget.toModelType());
+
+ return expenseTracker;
+ }
+
+ private Category getAssociatedCategory(Expense expense, ExpenseTracker expenseTracker) {
+ return expenseTracker.getCategoryInstance(expense.getCategory());
+ }
+
+ private Category getAssociatedCategoryForRecurring(RecurringExpenseManager recur, ExpenseTracker expenseTracker) {
+ return expenseTracker.getCategoryInstance(recur.getExpenseCategory());
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/fasttrack/storage/JsonUserPrefsStorage.java
similarity index 83%
rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java
rename to src/main/java/fasttrack/storage/JsonUserPrefsStorage.java
index bc2bbad84aa..eb89bdcd9cd 100644
--- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java
+++ b/src/main/java/fasttrack/storage/JsonUserPrefsStorage.java
@@ -1,13 +1,13 @@
-package seedu.address.storage;
+package fasttrack.storage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
-import seedu.address.commons.exceptions.DataConversionException;
-import seedu.address.commons.util.JsonUtil;
-import seedu.address.model.ReadOnlyUserPrefs;
-import seedu.address.model.UserPrefs;
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.commons.util.JsonUtil;
+import fasttrack.model.ReadOnlyUserPrefs;
+import fasttrack.model.UserPrefs;
/**
* A class to access UserPrefs stored in the hard disk as a json file
diff --git a/src/main/java/fasttrack/storage/Storage.java b/src/main/java/fasttrack/storage/Storage.java
new file mode 100644
index 00000000000..8f18507031b
--- /dev/null
+++ b/src/main/java/fasttrack/storage/Storage.java
@@ -0,0 +1,32 @@
+package fasttrack.storage;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.ReadOnlyUserPrefs;
+import fasttrack.model.UserPrefs;
+
+/**
+ * API of the Storage component
+ */
+public interface Storage extends ExpenseTrackerStorage, UserPrefsStorage {
+
+ @Override
+ Optional readUserPrefs() throws DataConversionException, IOException;
+
+ @Override
+ void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException;
+
+ @Override
+ Path getExpenseTrackerFilePath();
+
+ @Override
+ Optional readExpenseTracker() throws DataConversionException, IOException;
+
+ @Override
+ void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException;
+
+}
diff --git a/src/main/java/fasttrack/storage/StorageManager.java b/src/main/java/fasttrack/storage/StorageManager.java
new file mode 100644
index 00000000000..9da84ece82e
--- /dev/null
+++ b/src/main/java/fasttrack/storage/StorageManager.java
@@ -0,0 +1,78 @@
+package fasttrack.storage;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.model.ReadOnlyExpenseTracker;
+import fasttrack.model.ReadOnlyUserPrefs;
+import fasttrack.model.UserPrefs;
+
+/**
+ * Manages storage of ExpenseTracker data in local storage.
+ */
+public class StorageManager implements Storage {
+
+ private static final Logger logger = LogsCenter.getLogger(StorageManager.class);
+ private final ExpenseTrackerStorage expenseBookStorage;
+ private final UserPrefsStorage userPrefsStorage;
+
+ /**
+ * Creates a {@code StorageManager} with the given {@code ExpenseTrackerStorage}
+ * and {@code UserPrefStorage}.
+ */
+ public StorageManager(ExpenseTrackerStorage expenseTracker, UserPrefsStorage userPrefsStorage) {
+ this.expenseBookStorage = expenseTracker;
+ this.userPrefsStorage = userPrefsStorage;
+ }
+
+ // ================ UserPrefs methods ==============================
+
+ @Override
+ public Path getUserPrefsFilePath() {
+ return userPrefsStorage.getUserPrefsFilePath();
+ }
+
+ @Override
+ public Optional readUserPrefs() throws DataConversionException, IOException {
+ return userPrefsStorage.readUserPrefs();
+ }
+
+ @Override
+ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException {
+ userPrefsStorage.saveUserPrefs(userPrefs);
+ }
+
+ // ================ ExpenseTracker methods ==============================
+
+ @Override
+ public Path getExpenseTrackerFilePath() {
+ return expenseBookStorage.getExpenseTrackerFilePath();
+ }
+
+ @Override
+ public Optional readExpenseTracker() throws DataConversionException, IOException {
+ return readExpenseTracker(expenseBookStorage.getExpenseTrackerFilePath());
+ }
+
+ @Override
+ public Optional readExpenseTracker(Path filePath)
+ throws DataConversionException, IOException {
+ logger.fine("Attempting to read data from file: " + filePath);
+ return expenseBookStorage.readExpenseTracker(filePath);
+ }
+
+ @Override
+ public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException {
+ saveExpenseTracker(expenseTracker, expenseBookStorage.getExpenseTrackerFilePath());
+ }
+
+ @Override
+ public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, Path filePath) throws IOException {
+ logger.fine("Attempting to write to data file: " + filePath);
+ expenseBookStorage.saveExpenseTracker(expenseTracker, filePath);
+ }
+}
diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/fasttrack/storage/UserPrefsStorage.java
similarity index 71%
rename from src/main/java/seedu/address/storage/UserPrefsStorage.java
rename to src/main/java/fasttrack/storage/UserPrefsStorage.java
index 29eef178dbc..2f54a4fd8b2 100644
--- a/src/main/java/seedu/address/storage/UserPrefsStorage.java
+++ b/src/main/java/fasttrack/storage/UserPrefsStorage.java
@@ -1,15 +1,15 @@
-package seedu.address.storage;
+package fasttrack.storage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
-import seedu.address.commons.exceptions.DataConversionException;
-import seedu.address.model.ReadOnlyUserPrefs;
-import seedu.address.model.UserPrefs;
+import fasttrack.commons.exceptions.DataConversionException;
+import fasttrack.model.ReadOnlyUserPrefs;
+import fasttrack.model.UserPrefs;
/**
- * Represents a storage for {@link seedu.address.model.UserPrefs}.
+ * Represents a storage for {@link fasttrack.model.UserPrefs}.
*/
public interface UserPrefsStorage {
@@ -27,7 +27,7 @@ public interface UserPrefsStorage {
Optional readUserPrefs() throws DataConversionException, IOException;
/**
- * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage.
+ * Saves the given {@link fasttrack.model.ReadOnlyUserPrefs} to the storage.
* @param userPrefs cannot be null.
* @throws IOException if there was any problem writing to the file.
*/
diff --git a/src/main/java/fasttrack/ui/CategoryCard.java b/src/main/java/fasttrack/ui/CategoryCard.java
new file mode 100644
index 00000000000..a3c019f0075
--- /dev/null
+++ b/src/main/java/fasttrack/ui/CategoryCard.java
@@ -0,0 +1,57 @@
+package fasttrack.ui;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.util.UserInterfaceUtil;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+/**
+ * A UI component that displays information of a {@code Category}.
+ */
+public class CategoryCard extends UiPart {
+
+ private static final String FXML = "CategoryListCard.fxml";
+
+
+ public final Category category;
+
+ @FXML
+ private HBox cardPane;
+ @FXML
+ private Label id;
+ @FXML
+ private Label categoryName;
+ @FXML
+ private Label expenseCount;
+
+ /**
+ * Creates a {@code CategoryCard} with the given {@code Category} and index to display.
+ */
+ public CategoryCard(Category category, int displayedIndex, int associatedExpenseCount) {
+ super(FXML);
+ this.category = category;
+ id.setText(displayedIndex + ". ");
+ categoryName.setText(UserInterfaceUtil.capitalizeFirstLetter(category.getCategoryName()));
+ expenseCount.setText(String.valueOf(associatedExpenseCount));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof CategoryCard)) {
+ return false;
+ }
+
+ // state check
+ CategoryCard card = (CategoryCard) other;
+ return id.getText().equals(card.id.getText())
+ && category.equals(card.category);
+ }
+}
diff --git a/src/main/java/fasttrack/ui/CategoryListPanel.java b/src/main/java/fasttrack/ui/CategoryListPanel.java
new file mode 100644
index 00000000000..1bedc02fa46
--- /dev/null
+++ b/src/main/java/fasttrack/ui/CategoryListPanel.java
@@ -0,0 +1,54 @@
+package fasttrack.ui;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.layout.Region;
+
+/**
+ * Panel containing the list of categories.
+ */
+public class CategoryListPanel extends UiPart {
+ private static final String FXML = "CategoryListPanel.fxml";
+ private final ObservableList expenseObservableList;
+
+ @FXML
+ private ListView categoryListView;
+
+ /**
+ * Creates a {@code CategoryListPanel} with the given {@code ObservableList}.
+ */
+ public CategoryListPanel(ObservableList categoryList, ObservableList expenseList) {
+ super(FXML);
+ this.expenseObservableList = expenseList;
+ categoryListView.setItems(categoryList);
+ categoryListView.setCellFactory(listView -> new CategoryListViewCell());
+ }
+
+ private int getAssociatedExpenseCount(Category category) {
+ return (int) expenseObservableList.stream()
+ .filter(e -> e.getCategory().equals(category))
+ .count();
+ }
+
+ /**
+ * Custom {@code ListCell} that displays the graphics of a {@code Category} using a {@code CategoryCard}.
+ */
+ class CategoryListViewCell extends ListCell {
+ @Override
+ protected void updateItem(Category category, boolean empty) {
+ super.updateItem(category, empty);
+
+ if (empty || category == null) {
+ setGraphic(null);
+ setText(null);
+ } else {
+ setGraphic(new CategoryCard(category, getIndex() + 1, getAssociatedExpenseCount(category)).getRoot());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/fasttrack/ui/CommandBox.java b/src/main/java/fasttrack/ui/CommandBox.java
new file mode 100644
index 00000000000..7aad050dbe2
--- /dev/null
+++ b/src/main/java/fasttrack/ui/CommandBox.java
@@ -0,0 +1,164 @@
+package fasttrack.ui;
+
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.logic.parser.exceptions.ParseException;
+import javafx.beans.property.StringProperty;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.Region;
+import javafx.stage.Window;
+
+/**
+ * The UI component that is responsible for receiving user command inputs.
+ */
+public class CommandBox extends UiPart {
+
+ public static final String ERROR_STYLE_CLASS = "error";
+ private static final String FXML = "CommandBox.fxml";
+
+ private final CommandExecutor commandExecutor;
+
+ @FXML
+ private TextField commandTextField;
+
+ /**
+ * Creates a {@code CommandBox} with the given {@code CommandExecutor}.
+ */
+ public CommandBox(CommandExecutor commandExecutor, boolean initialiseAutocompletion) {
+ super(FXML);
+ this.commandExecutor = commandExecutor;
+ // calls #setStyleToDefault() whenever there is a change to the text of the command box.
+ commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault());
+ if (initialiseAutocompletion) {
+ initialiseAutocompleteHandler();
+ }
+ }
+
+
+ /**
+ * Handles the Enter button pressed event.
+ */
+ @FXML
+ public void handleCommandEntered() {
+ String commandText = commandTextField.getText();
+ if (commandText.equals("")) {
+ return;
+ }
+
+ try {
+ commandExecutor.execute(commandText);
+ commandTextField.setText("");
+ } catch (CommandException | ParseException e) {
+ setStyleToIndicateCommandFailure();
+ }
+ }
+
+ /**
+ * Sets the command box style to use the default style.
+ */
+ private void setStyleToDefault() {
+ commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS);
+ }
+
+ /**
+ * Sets the command box style to indicate a failed command.
+ */
+ private void setStyleToIndicateCommandFailure() {
+ ObservableList styleClass = commandTextField.getStyleClass();
+
+ if (styleClass.contains(ERROR_STYLE_CLASS)) {
+ return;
+ }
+
+ styleClass.add(ERROR_STYLE_CLASS);
+ }
+
+ /**
+ * Returns the current text property of the command text field
+ * @return the text property of the command text field
+ */
+ public StringProperty getTextProperty() {
+ return commandTextField.textProperty();
+ }
+
+ /**
+ * Gives back focus to the command text field and positions the cursor at the end of the text.
+ */
+ public void setFocus() {
+ commandTextField.requestFocus();
+ commandTextField.positionCaret(commandTextField.getText().length());
+ }
+
+ /**
+ * Updates the command text field with the given category name by
+ * replacing the text after "c/" with the category name.
+ * @param categoryName The name of the category to be inserted into the command text field.
+ */
+ public void updateCommandBoxText(String categoryName) {
+ String currentText = commandTextField.getText();
+ int inputIndex = currentText.indexOf("c/") + 2;
+ String updatedString;
+ updatedString = currentText.substring(0, inputIndex) + categoryName;
+ commandTextField.setText(updatedString + " ");
+ }
+
+ /**
+ * Adds a key press event filter to the command text field that listens for the UP arrow key.
+ * When the up arrow key is pressed, focus is given to the suggestion list if it is visible.
+ */
+ private void initialiseAutocompleteHandler() {
+ commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
+ Window mainStage = commandTextField.getScene().getWindow();
+ Node suggestionsList = mainStage.getScene().lookup("#suggestionListView");
+ if (event.getCode() == KeyCode.UP && suggestionsList.isVisible()) {
+ suggestionsList.requestFocus();
+ }
+ if (event.getCode() == KeyCode.ENTER && suggestionsList.isVisible()) {
+ suggestionsList.setVisible(false);
+ }
+ if (event.getCode() == KeyCode.TAB) {
+ // simulate UP key press
+ KeyEvent upEvent = new KeyEvent(
+ KeyEvent.KEY_PRESSED,
+ "",
+ "",
+ KeyCode.UP,
+ false,
+ false,
+ false,
+ false);
+ commandTextField.fireEvent(upEvent);
+ // simulate ENTER key press
+ KeyEvent enterEvent = new KeyEvent(
+ KeyEvent.KEY_PRESSED,
+ "",
+ "", KeyCode.ENTER,
+ false,
+ false,
+ false,
+ false);
+ suggestionsList.fireEvent(enterEvent);
+ event.consume();
+ }
+ });
+ }
+
+ /**
+ * Represents a function that can execute commands.
+ */
+ @FunctionalInterface
+ public interface CommandExecutor {
+ /**
+ * Executes the command and returns the result.
+ *
+ * @see fasttrack.logic.Logic#execute(String)
+ */
+ CommandResult execute(String commandText) throws CommandException, ParseException;
+ }
+
+}
diff --git a/src/main/java/fasttrack/ui/ExpenseCard.java b/src/main/java/fasttrack/ui/ExpenseCard.java
new file mode 100644
index 00000000000..cc9da2ff5b7
--- /dev/null
+++ b/src/main/java/fasttrack/ui/ExpenseCard.java
@@ -0,0 +1,65 @@
+package fasttrack.ui;
+
+import fasttrack.model.expense.Expense;
+import fasttrack.model.util.UserInterfaceUtil;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+/**
+ * A UI component that displays information of a {@code Expense}.
+ */
+public class ExpenseCard extends UiPart {
+
+ private static final String FXML = "ExpenseListCard.fxml";
+
+
+ public final Expense expense;
+
+ @FXML
+ private HBox cardPane;
+ @FXML
+ private Label id;
+ @FXML
+ private Label expenseName;
+ @FXML
+ private Label price;
+ @FXML
+ private Label category;
+ @FXML
+ private Label date;
+
+
+ /**
+ * Creates a {@code ExpenseCard} with the given {@code Expense} and index to display.
+ */
+ public ExpenseCard(Expense expense, int displayedIndex) {
+ super(FXML);
+ this.expense = expense;
+ id.setText(displayedIndex + ". ");
+ expenseName.setText(UserInterfaceUtil.capitalizeFirstLetter(expense.getName()));
+ String categoryName = expense.getCategory().getCategoryName();
+ category.setText(UserInterfaceUtil.capitalizeFirstLetter(categoryName));
+ date.setText(UserInterfaceUtil.parseDate(expense.getDate()));
+ price.setText(UserInterfaceUtil.parsePrice(expense.getAmount()));
+ }
+
+ @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/fasttrack/ui/ExpenseListPanel.java b/src/main/java/fasttrack/ui/ExpenseListPanel.java
new file mode 100644
index 00000000000..6265ab34ec6
--- /dev/null
+++ b/src/main/java/fasttrack/ui/ExpenseListPanel.java
@@ -0,0 +1,56 @@
+package fasttrack.ui;
+
+import java.util.logging.Logger;
+
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.model.expense.Expense;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.layout.Region;
+
+/**
+ * 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;
+
+ /**
+ * Creates a {@code ExpenseListPanel} with the given {@code ObservableList}.
+ */
+ public ExpenseListPanel(ObservableList expenseList) {
+ super(FXML);
+ expenseListView.setItems(expenseList);
+ expenseListView.setCellFactory(listView -> new ExpenseListViewCell());
+ }
+
+ /**
+ * Refreshes the list of expenses and its related data.
+ */
+ public void refreshList() {
+ expenseListView.refresh();
+ }
+
+ /**
+ * 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/fasttrack/ui/HelpWindow.java b/src/main/java/fasttrack/ui/HelpWindow.java
new file mode 100644
index 00000000000..f232e256f28
--- /dev/null
+++ b/src/main/java/fasttrack/ui/HelpWindow.java
@@ -0,0 +1,140 @@
+package fasttrack.ui;
+
+import java.util.logging.Logger;
+
+import fasttrack.commons.core.LogsCenter;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.stage.Stage;
+
+/**
+ * Controller for a help page
+ */
+public class HelpWindow extends UiPart {
+
+ public static final String USERGUIDE_URL =
+ "https://ay2223s2-cs2103t-w09-2.github.io/tp/UserGuide.html";
+ public static final String HELP_MESSAGE_COMMAND = "Features:\n"
+ + "help\n"
+ + " - Accesses this help guide\n\n"
+ + "addcat c/categoryName\n"
+ + " - Adds a new category to the expense tracker\n\n"
+ + "delcat index\n"
+ + " - Deletes a category from the expense tracker\n\n"
+ + "edcat index c/categoryName [s/summary]\n"
+ + " - Edits specified category in the expense tracker\n\n"
+ + "lcat\n"
+ + " - Shows all categories in the expense tracker\n\n"
+ + "add n/expenseName c/categoryName p/price [d/Date]\n"
+ + " - Adds an expense to the user's expense tracker\n\n"
+ + "delete index\n"
+ + " - Deletes expense at index [index]\n\n"
+ + "edexp index [c/categoryName] [n/expenseName] [d/Date] [p/price]\n"
+ + " - Edits specified expense in the expense tracker\n\n"
+ + "addrec c/categoryName n/itemName p/price t/timeframe sd/startDate [ed/endDate]\n"
+ + " - Adds a recurring expense to the user's expense tracker\n\n"
+ + "delrec index\n"
+ + " - Deletes a recurring expense at index [index]\n\n"
+ + "edrec index [c/categoryName] [n/itemName] [p/price] [t/timeframe] [ed/endDate]\n"
+ + " - Edits specified expense in the expense tracker\n\n"
+ + "lrec\n"
+ + " - Displays the list of recurring expenses\n\n"
+ + "set p/budget\n"
+ + " - Sets the monthly budget in the expense tracker\n\n"
+ + "scat index\n"
+ + " - Displays summary text of category\n\n"
+ + "find [keyword]\n"
+ + " - lists all expenses with matching keywords\n\n"
+ + "list [c/categoryName] [t/timeframe]\n"
+ + " - Displays the list of expenses with optional category or time-span filters\n\n"
+ + "CLEAR\n"
+ + " - Clears all the expenses and categories stored in FastTrack\n\n";
+
+ public static final String HELP_MESSAGE = "For more info, refer to the FastTrack user guide: "
+ + USERGUIDE_URL;
+
+ private static final Logger logger = LogsCenter.getLogger(HelpWindow.class);
+ private static final String FXML = "HelpWindow.fxml";
+
+ @FXML
+ private Button copyButton;
+
+ @FXML
+ private Label helpMessage;
+
+ /**
+ * Creates a new HelpWindow.
+ *
+ * @param root Stage to use as the root of the HelpWindow.
+ */
+ public HelpWindow(Stage root) {
+ super(FXML, root);
+ helpMessage.setText(HELP_MESSAGE);
+ }
+
+ /**
+ * Creates a new HelpWindow.
+ */
+ public HelpWindow() {
+ this(new Stage());
+ }
+
+ /**
+ * Shows the help window.
+ * @throws IllegalStateException
+ *
+ * -
+ * if this method is called on a thread other than the JavaFX Application Thread.
+ *
+ * -
+ * if this method is called during animation or layout processing.
+ *
+ * -
+ * if this method is called on the primary stage.
+ *
+ * -
+ * if {@code dialogStage} is already showing.
+ *
+ *
+ */
+ public void show() {
+ logger.fine("Showing help page about the application.");
+ getRoot().show();
+ getRoot().centerOnScreen();
+ }
+
+ /**
+ * Returns true if the help window is currently being shown.
+ */
+ public boolean isShowing() {
+ return getRoot().isShowing();
+ }
+
+ /**
+ * Hides the help window.
+ */
+ public void hide() {
+ getRoot().hide();
+ }
+
+ /**
+ * Focuses on the help window.
+ */
+ public void focus() {
+ getRoot().requestFocus();
+ }
+
+ /**
+ * Copies the URL to the user guide to the clipboard.
+ */
+ @FXML
+ private void copyUrl() {
+ final Clipboard clipboard = Clipboard.getSystemClipboard();
+ final ClipboardContent url = new ClipboardContent();
+ url.putString(USERGUIDE_URL);
+ clipboard.setContent(url);
+ }
+}
diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/fasttrack/ui/MainWindow.java
similarity index 65%
rename from src/main/java/seedu/address/ui/MainWindow.java
rename to src/main/java/fasttrack/ui/MainWindow.java
index 9106c3aa6e5..b398df58fe8 100644
--- a/src/main/java/seedu/address/ui/MainWindow.java
+++ b/src/main/java/fasttrack/ui/MainWindow.java
@@ -1,21 +1,23 @@
-package seedu.address.ui;
+package fasttrack.ui;
import java.util.logging.Logger;
+import fasttrack.commons.core.GuiSettings;
+import fasttrack.commons.core.LogsCenter;
+import fasttrack.logic.Logic;
+import fasttrack.logic.commands.CommandResult;
+import fasttrack.logic.commands.exceptions.CommandException;
+import fasttrack.logic.parser.exceptions.ParseException;
+import fasttrack.model.AnalyticModelManager;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
+import javafx.scene.Parent;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
-import seedu.address.commons.core.GuiSettings;
-import seedu.address.commons.core.LogsCenter;
-import seedu.address.logic.Logic;
-import seedu.address.logic.commands.CommandResult;
-import seedu.address.logic.commands.exceptions.CommandException;
-import seedu.address.logic.parser.exceptions.ParseException;
/**
* The Main Window. Provides the basic application layout containing
@@ -31,9 +33,12 @@ public class MainWindow extends UiPart {
private Logic logic;
// Independent Ui parts residing in this Ui container
- private PersonListPanel personListPanel;
+ private ExpenseListPanel expenseListPanel;
private ResultDisplay resultDisplay;
private HelpWindow helpWindow;
+ private ResultsHeader resultsHeader;
+ private ResultsDetails resultsDetails;
+ private StatisticsPanel statisticsPanel;
@FXML
private StackPane commandBoxPlaceholder;
@@ -42,7 +47,7 @@ public class MainWindow extends UiPart {
private MenuItem helpMenuItem;
@FXML
- private StackPane personListPanelPlaceholder;
+ private StackPane listPanelPlaceholder;
@FXML
private StackPane resultDisplayPlaceholder;
@@ -50,6 +55,16 @@ public class MainWindow extends UiPart {
@FXML
private StackPane statusbarPlaceholder;
+ @FXML
+ private StackPane resultsHeaderPlaceholder;
+
+ @FXML
+ private StackPane resultsDetailsPlaceholder;
+
+ @FXML
+ private StackPane statisticsPlaceholder;
+
+
/**
* Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}.
*/
@@ -110,17 +125,33 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) {
* Fills up all the placeholders of this window.
*/
void fillInnerParts() {
- personListPanel = new PersonListPanel(logic.getFilteredPersonList());
- personListPanelPlaceholder.getChildren().add(personListPanel.getRoot());
+ expenseListPanel = new ExpenseListPanel(logic.getFilteredExpenseList());
+ listPanelPlaceholder.getChildren().add(expenseListPanel.getRoot());
resultDisplay = new ResultDisplay();
resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot());
+ resultsHeader = new ResultsHeader(logic.getAppliedCategoryFilter());
+ resultsHeaderPlaceholder.getChildren().add(resultsHeader.getRoot());
+
+ resultsDetails = new ResultsDetails(logic.getFilteredExpenseList(),
+ logic.getRecurringExpenseManagerList(),
+ logic.getFilteredCategoryList(), logic.getAppliedTimeSpanFilter());
+ resultsDetailsPlaceholder.getChildren().add(resultsDetails.getRoot());
+
+ statisticsPanel = new StatisticsPanel(new AnalyticModelManager(logic.getExpenseTracker()));
+ statisticsPlaceholder.getChildren().add(statisticsPanel.getRoot());
+
StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath());
statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot());
- CommandBox commandBox = new CommandBox(this::executeCommand);
+ CommandBox commandBox = new CommandBox(this::executeCommand, true);
+ SuggestionListPanel suggestionListPanel = new SuggestionListPanel(logic.getFilteredCategoryList(), commandBox);
+
+
commandBoxPlaceholder.getChildren().add(commandBox.getRoot());
+ resultDisplayPlaceholder.getChildren().add(suggestionListPanel.getRoot());
+
}
/**
@@ -163,29 +194,49 @@ private void handleExit() {
primaryStage.hide();
}
- public PersonListPanel getPersonListPanel() {
- return personListPanel;
+ /**
+ * Toggles the display between the different screens
+ * @param screenType the screen to be shown
+ */
+ public void switchListPanel(ScreenType screenType) {
+ resultsHeader.setHeader(screenType);
+ resultsDetails.switchDetails(screenType);
+ Parent listPanelRoot;
+ switch (screenType) {
+ case EXPENSE_SCREEN:
+ listPanelRoot = new ExpenseListPanel(logic.getFilteredExpenseList()).getRoot();
+ break;
+ case CATEGORY_SCREEN:
+ listPanelRoot = new CategoryListPanel(logic.getFilteredCategoryList(),
+ logic.getFilteredExpenseList()).getRoot();
+ break;
+ case RECURRING_EXPENSE_SCREEN:
+ listPanelRoot = new RecurringExpensePanel(logic.getRecurringExpenseManagerList()).getRoot();
+ break;
+ default:
+ throw new IllegalArgumentException("Screen type does not exist");
+ }
+ listPanelPlaceholder.getChildren().setAll(listPanelRoot);
}
/**
* Executes the command and returns the result.
*
- * @see seedu.address.logic.Logic#execute(String)
+ * @see fasttrack.logic.Logic#execute(String)
*/
private CommandResult executeCommand(String commandText) throws CommandException, ParseException {
try {
CommandResult commandResult = logic.execute(commandText);
+ expenseListPanel.refreshList();
logger.info("Result: " + commandResult.getFeedbackToUser());
resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());
-
+ switchListPanel(commandResult.getScreenType());
if (commandResult.isShowHelp()) {
handleHelp();
}
-
if (commandResult.isExit()) {
handleExit();
}
-
return commandResult;
} catch (CommandException | ParseException e) {
logger.info("Invalid command: " + commandText);
diff --git a/src/main/java/fasttrack/ui/RecurringExpenseCard.java b/src/main/java/fasttrack/ui/RecurringExpenseCard.java
new file mode 100644
index 00000000000..9fa270689f9
--- /dev/null
+++ b/src/main/java/fasttrack/ui/RecurringExpenseCard.java
@@ -0,0 +1,64 @@
+package fasttrack.ui;
+
+import fasttrack.model.expense.RecurringExpenseManager;
+import fasttrack.model.util.UserInterfaceUtil;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+/**
+ * A UI component that displays information of a
+ * {@code RecurringExpenseManager}.
+ */
+public class RecurringExpenseCard extends UiPart {
+
+ private static final String FXML = "RecurringExpenseListCard.fxml";
+
+ public final RecurringExpenseManager recurringExpenseManager;
+
+ @FXML
+ private HBox cardPane;
+ @FXML
+ private Label id;
+ @FXML
+ private Label expenseName;
+ @FXML
+ private Label price;
+ @FXML
+ private Label category;
+ @FXML
+ private Label frequency;
+
+ /**
+ * Creates a {@code RecurringExpenseCard} with the given
+ * {@code RecurringExpenseManager} and index to display.
+ */
+ public RecurringExpenseCard(RecurringExpenseManager recurringExpenseManager, int displayedIndex) {
+ super(FXML);
+ this.recurringExpenseManager = recurringExpenseManager;
+ id.setText(displayedIndex + ". ");
+ expenseName.setText(UserInterfaceUtil.capitalizeFirstLetter(recurringExpenseManager.getExpenseName()));
+ String categoryName = recurringExpenseManager.getExpenseCategory().getCategoryName();
+ category.setText(UserInterfaceUtil.capitalizeFirstLetter(categoryName));
+ price.setText(UserInterfaceUtil.parsePrice(recurringExpenseManager.getAmount()));
+ String recurringExpenseFrequency = recurringExpenseManager.getRecurringExpenseType().name();
+ frequency.setText(UserInterfaceUtil.capitalizeFirstLetter(recurringExpenseFrequency.toLowerCase()));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+ // instanceof handles nulls
+ if (!(other instanceof RecurringExpenseCard)) {
+ return false;
+ }
+ // state check
+ RecurringExpenseCard card = (RecurringExpenseCard) other;
+ return id.getText().equals(card.id.getText())
+ && recurringExpenseManager.equals(card.recurringExpenseManager);
+ }
+}
diff --git a/src/main/java/fasttrack/ui/RecurringExpensePanel.java b/src/main/java/fasttrack/ui/RecurringExpensePanel.java
new file mode 100644
index 00000000000..0853a3c8c8a
--- /dev/null
+++ b/src/main/java/fasttrack/ui/RecurringExpensePanel.java
@@ -0,0 +1,54 @@
+package fasttrack.ui;
+
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.layout.Region;
+
+
+/**
+ * Panel containing the list of recurring expenses.
+ */
+public class RecurringExpensePanel extends UiPart {
+ private static final String FXML = "RecurringExpenseListPanel.fxml";
+
+ @FXML
+ private ListView recurringExpenseListView;
+
+ /**
+ * Creates a {@code RecurringExpensePanel} with the given {@code ObservableList}.
+ */
+ public RecurringExpensePanel(ObservableList recurringExpenseList) {
+ super(FXML);
+ recurringExpenseListView.setItems(recurringExpenseList);
+ recurringExpenseListView.setCellFactory(listView -> new RecurringExpenseListViewCell());
+ }
+
+ /**
+ * Refreshes the list of recurring expenses and its related data.
+ */
+ public void refreshList() {
+ recurringExpenseListView.refresh();
+ }
+
+ /**
+ * Custom {@code ListCell} that displays the graphics of a {@code RecurringExpenseManager}
+ * using a {@code RecurringExpenseCard}.
+ */
+ class RecurringExpenseListViewCell extends ListCell {
+ @Override
+ protected void updateItem(RecurringExpenseManager expense, boolean empty) {
+ super.updateItem(expense, empty);
+
+ if (empty || expense == null) {
+ setGraphic(null);
+ setText(null);
+ } else {
+ setGraphic(new RecurringExpenseCard(expense, getIndex() + 1).getRoot());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/fasttrack/ui/ResultDisplay.java
similarity index 95%
rename from src/main/java/seedu/address/ui/ResultDisplay.java
rename to src/main/java/fasttrack/ui/ResultDisplay.java
index 7d98e84eedf..a670b4af1d6 100644
--- a/src/main/java/seedu/address/ui/ResultDisplay.java
+++ b/src/main/java/fasttrack/ui/ResultDisplay.java
@@ -1,4 +1,4 @@
-package seedu.address.ui;
+package fasttrack.ui;
import static java.util.Objects.requireNonNull;
diff --git a/src/main/java/fasttrack/ui/ResultsDetails.java b/src/main/java/fasttrack/ui/ResultsDetails.java
new file mode 100644
index 00000000000..b9f6e7a5518
--- /dev/null
+++ b/src/main/java/fasttrack/ui/ResultsDetails.java
@@ -0,0 +1,131 @@
+package fasttrack.ui;
+
+import fasttrack.logic.parser.ParserUtil;
+import fasttrack.model.category.Category;
+import fasttrack.model.expense.Expense;
+import fasttrack.model.expense.RecurringExpenseManager;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.IntegerBinding;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Region;
+import javafx.util.StringConverter;
+
+/**
+ * A UI component that displays information of a {@code Expense} or {@code Category}
+ * such as the number of results and the time range filter applied.
+ */
+public class ResultsDetails extends UiPart {
+
+ private static final String FXML = "ResultsDetails.fxml";
+ private final IntegerProperty count;
+ private final ObservableList expenseList;
+ private final ObservableList categoryList;
+ private final ObservableList recurringExpenseManagersList;
+
+ @FXML
+ private Label resultsCount;
+ @FXML
+ private Label dateFilter;
+ @FXML
+ private Label dateLabel;
+
+ /**
+ * Creates a new ResultsDetails pane to display the details of each data
+ * @param expenseList the list of expenses to display
+ * @param recurringExpenseManagersList the list of recurring expense managers to display
+ * @param categoryList the list of categories to display
+ * @param timeFilter the filter to apply to expense based on month, week, year
+ */
+ public ResultsDetails(ObservableList expenseList,
+ ObservableList recurringExpenseManagersList,
+ ObservableList categoryList,
+ SimpleObjectProperty timeFilter) {
+ super(FXML);
+ this.count = new SimpleIntegerProperty();
+ this.expenseList = expenseList;
+ this.categoryList = categoryList;
+ this.recurringExpenseManagersList = recurringExpenseManagersList;
+ dateLabel.setText("Date:");
+ bindResultsCount(ScreenType.EXPENSE_SCREEN);
+ dateFilter.textProperty().bindBidirectional(timeFilter, new CustomStringConverter());
+ }
+
+
+ /**
+ * Switches the details displayed on the GUI based on an enum indicating
+ * which screen is being displayed.
+ * @param screenType an enum indicating which screen is being displayed.
+ */
+ public void switchDetails(ScreenType screenType) {
+ bindResultsCount(screenType);
+ if (screenType == ScreenType.EXPENSE_SCREEN) {
+ dateLabel.setText("Time:");
+ } else if (screenType == ScreenType.CATEGORY_SCREEN || screenType == ScreenType.RECURRING_EXPENSE_SCREEN) {
+ dateLabel.setText("");
+ dateFilter.setText("");
+ }
+ }
+
+ /**
+ * Helper method used by the switchDetails method to bind the count of the number of items
+ * in the expenseList, categoryList or recurringExpenseManagerList to the count variable,
+ * which is then displayed in the GUI.
+ * @param screenType an enum indicating which screen is being displayed.
+ */
+ private void bindResultsCount(ScreenType screenType) {
+ if (screenType == ScreenType.EXPENSE_SCREEN) {
+ IntegerBinding expenseListSizeBinding = Bindings.size(expenseList);
+ count.bind(expenseListSizeBinding);
+ } else if (screenType == ScreenType.CATEGORY_SCREEN) {
+ IntegerBinding categoryListSizeBinding = Bindings.size(categoryList);
+ count.bind(categoryListSizeBinding);
+ } else if (screenType == ScreenType.RECURRING_EXPENSE_SCREEN) {
+ IntegerBinding recurringExpenseListSizeBinding = Bindings.size(recurringExpenseManagersList);
+ count.bind(recurringExpenseListSizeBinding);
+ }
+ resultsCount.textProperty().bind(count.asString());
+ }
+
+
+ /**
+ * A custom string converter for the date filter.
+ */
+ private static class CustomStringConverter extends StringConverter {
+ @Override
+ public String toString(ParserUtil.Timespan myEnum) {
+ return myEnum.toString();
+ }
+ /**
+ * Not implemented for this class, always returns null.
+ * @param string the string to convert
+ * @return null
+ */
+ @Override
+ public ParserUtil.Timespan fromString(String string) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ResultsDetails)) {
+ return false;
+ }
+
+ // state check
+ ResultsDetails details = (ResultsDetails) other;
+ return resultsCount.getText().equals(details.resultsCount.getText())
+ && dateFilter.getText().equals(details.dateFilter.getText());
+ }
+}
diff --git a/src/main/java/fasttrack/ui/ResultsHeader.java b/src/main/java/fasttrack/ui/ResultsHeader.java
new file mode 100644
index 00000000000..1c788eb5a50
--- /dev/null
+++ b/src/main/java/fasttrack/ui/ResultsHeader.java
@@ -0,0 +1,91 @@
+package fasttrack.ui;
+
+import fasttrack.model.category.Category;
+import fasttrack.model.util.UserInterfaceUtil;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Region;
+import javafx.util.StringConverter;
+
+/**
+ * A UI component that displays information of a {@code Expense} or {@code Category}
+ * with a title and the category filter applied if any.
+ */
+public class ResultsHeader extends UiPart {
+
+ private static final String FXML = "ResultsHeader.fxml";
+
+ @FXML
+ private Label resultType;
+ @FXML
+ private Label filterType;
+
+ /**
+ * Creates a {@code ResultsHeader} with the given {@code categoryFilter}.
+ */
+ public ResultsHeader(SimpleObjectProperty categoryFilter) {
+ super(FXML);
+ filterType.textProperty().bindBidirectional(categoryFilter, new CategoryStringConverter());
+ resultType.setText("Expenses");
+ }
+
+ /**
+ * Sets the header of the results pane based on the given screen type.
+ * @param screenType the type of screen to set the header for
+ */
+ public void setHeader(ScreenType screenType) {
+ if (screenType == ScreenType.EXPENSE_SCREEN) {
+ resultType.setText("Expenses");
+ return;
+ } else if (screenType == ScreenType.CATEGORY_SCREEN) {
+ resultType.setText("Category");
+ } else if (screenType == ScreenType.RECURRING_EXPENSE_SCREEN) {
+ resultType.setText("Recurring Expenses");
+ }
+ filterType.setText("");
+ }
+
+ /**
+ * A custom string converter that converts a Category object to its capitalized category name,
+ * or "All" if the category is null.
+ * This is used to display the currently selected category filter, which can be null if no
+ * category filter is selected
+ */
+ public static class CategoryStringConverter extends StringConverter {
+ @Override
+ public String toString(Category category) {
+ if (category != null) {
+ return UserInterfaceUtil.capitalizeFirstLetter(category.getCategoryName());
+ }
+ return "All";
+ }
+ /**
+ * Not implemented for this class, always returns null.
+ * @param string the string to convert
+ * @return null
+ */
+ @Override
+ public Category fromString(String string) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ResultsHeader)) {
+ return false;
+ }
+
+ // state check
+ ResultsHeader header = (ResultsHeader) other;
+ return resultType.getText().equals(header.resultType.getText())
+ && filterType.getText().equals(header.filterType.getText());
+ }
+}
diff --git a/src/main/java/fasttrack/ui/ScreenType.java b/src/main/java/fasttrack/ui/ScreenType.java
new file mode 100644
index 00000000000..9f03639634f
--- /dev/null
+++ b/src/main/java/fasttrack/ui/ScreenType.java
@@ -0,0 +1,12 @@
+package fasttrack.ui;
+
+/**
+ * Represents different types of analytics that can be calculated in FastTrack
+ * MONTHLY_SPENT: Represents the total amount spent in a month.
+ */
+
+public enum ScreenType {
+ EXPENSE_SCREEN,
+ CATEGORY_SCREEN,
+ RECURRING_EXPENSE_SCREEN
+}
diff --git a/src/main/java/fasttrack/ui/StatisticsPanel.java b/src/main/java/fasttrack/ui/StatisticsPanel.java
new file mode 100644
index 00000000000..faeacdca14a
--- /dev/null
+++ b/src/main/java/fasttrack/ui/StatisticsPanel.java
@@ -0,0 +1,184 @@
+package fasttrack.ui;
+
+import fasttrack.model.AnalyticModel;
+import fasttrack.model.util.AnalyticsType;
+import javafx.beans.property.DoubleProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+/**
+ * The UI component that is responsible for displaying user spending statistics
+ */
+public class StatisticsPanel extends UiPart {
+
+ private static final String FXML = "StatisticsPanel.fxml";
+ private final AnalyticModel analyticModel;
+
+ @FXML
+ private Label monthlySpending;
+ @FXML
+ private Label weeklySpending;
+ @FXML
+ private Label monthlyRemaining;
+ @FXML
+ private Label weeklyRemaining;
+ @FXML
+ private Label monthlyChange;
+ @FXML
+ private Label weeklyChange;
+ @FXML
+ private Label totalSpending;
+ @FXML
+ private Label budgetPercentage;
+ @FXML
+ private Label monthlySign;
+ @FXML
+ private Label weeklySign;
+ @FXML
+ private Label budgetAdvice;
+ @FXML
+ private HBox weeklyChangeBackground;
+ @FXML
+ private HBox monthlyChangeBackground;
+
+ /**
+ * Creates a new StatisticsPanel object with the specified AnalyticModel object
+ * and binds all values to the statistics
+ * @param analyticModel the AnalyticModel object for data retrieval
+ */
+ public StatisticsPanel(AnalyticModel analyticModel) {
+ super(FXML);
+ this.analyticModel = analyticModel;
+ bindAllValuesToStatistics();
+ updateBudgetAdvice(analyticModel.getMonthlySpent().doubleValue());
+ analyticModel.getMonthlySpent().addListener((observable, oldValue, newValue) -> {
+ updateBudgetAdvice(newValue);
+ });
+ analyticModel.getMonthlyBudgetProperty().addListener((observable, oldValue, newValue) -> {
+ updateBudgetAdvice(analyticModel.getMonthlySpent().get());
+ });
+ }
+
+ /**
+ * Binds the given AnalyticsType value to the given Label object
+ * and formats the label text as a price or percentage
+ * @param analyticsType the AnalyticsType value to bind
+ * @param labelToBind the Label object to bind the value to
+ * @param isPrice true if the value should be formatted as a price, false if to be formatted as a percentage
+ */
+ private void bindValueToStatistic(AnalyticsType analyticsType, Label labelToBind, boolean isPrice) {
+ String formatString = isPrice ? "$%.2f" : "%.2f%%";
+ labelToBind.textProperty().bind(analyticModel.getAnalyticsData(analyticsType).asString(formatString));
+ }
+
+ /**
+ * Binds the given AnalyticsType value to the given Label object
+ * and updates the percentage change indicator style based on the value
+ * @param analyticsType the AnalyticsType value to bind
+ */
+ private void bindValueToChangeIndicator(AnalyticsType analyticsType) {
+ DoubleProperty changeValue = analyticModel.getAnalyticsData(analyticsType);
+ if (analyticsType != AnalyticsType.WEEKLY_CHANGE
+ && analyticsType != AnalyticsType.MONTHLY_CHANGE) {
+ throw new IllegalArgumentException(
+ "The change indicator only accepts MONTHLY_CHANGE or WEEKLY_CHANGE!");
+ }
+ // Updating weekly change indicator
+ if (analyticsType == AnalyticsType.WEEKLY_CHANGE) {
+ weeklyChange.textProperty().bind(changeValue.asString("%.2f%%"));
+ updateChangeIndicatorStyles(
+ weeklyChange, weeklySign, weeklyChangeBackground, changeValue.doubleValue());
+ changeValue.addListener((observable, oldValue, newValue) -> {
+ updateChangeIndicatorStyles(
+ weeklyChange, weeklySign, weeklyChangeBackground, newValue.doubleValue());
+ });
+ } else {
+ // Updating monthly change indicator
+ monthlyChange.textProperty().bind(changeValue.asString("%.2f%%"));
+ updateChangeIndicatorStyles(
+ monthlyChange, monthlySign, monthlyChangeBackground, changeValue.doubleValue());
+ changeValue.addListener((observable, oldValue, newValue) -> {
+ updateChangeIndicatorStyles(
+ monthlyChange, monthlySign, monthlyChangeBackground, newValue.doubleValue());
+ });
+ }
+ }
+
+ private void updateChangeIndicatorStyles(Label labelToUpdate,
+ Label signToUpdate,
+ HBox backgroundToUpdate, double value) {
+ // Determine the CSS classes to apply based on the value of the change
+ String textColorClass = (value > 0)
+ ? "negative_change_indicator"
+ : "positive_change_indicator";
+ String backgroundColorClass = (value > 0)
+ ? "change_indicator_background_negative"
+ : "change_indicator_background_positive";
+ // Add sign label with a plus if the value is positive
+ signToUpdate.setText((value >= 0) ? "+" : "");
+ // Update the style classes with new background
+ signToUpdate.getStyleClass().removeAll("negative_change_indicator",
+ "positive_change_indicator");
+ signToUpdate.getStyleClass().add(textColorClass);
+ backgroundToUpdate.getStyleClass()
+ .removeAll("change_indicator_background_positive",
+ "change_indicator_background_negative");
+ backgroundToUpdate.getStyleClass().add(backgroundColorClass);
+ // Update the main labels showing the values
+ labelToUpdate.getStyleClass()
+ .removeAll("negative_change_indicator",
+ "positive_change_indicator");
+ labelToUpdate.getStyleClass().add(textColorClass);
+ }
+
+ private void bindAllValuesToStatistics() {
+ bindValueToStatistic(AnalyticsType.MONTHLY_SPENT, monthlySpending, true);
+ bindValueToStatistic(AnalyticsType.MONTHLY_REMAINING, monthlyRemaining, true);
+ bindValueToStatistic(AnalyticsType.WEEKLY_SPENT, weeklySpending, true);
+ bindValueToStatistic(AnalyticsType.WEEKLY_REMAINING, weeklyRemaining, true);
+ bindValueToStatistic(AnalyticsType.TOTAL_SPENT, totalSpending, true);
+ bindValueToStatistic(AnalyticsType.BUDGET_PERCENTAGE, budgetPercentage, false);
+ bindValueToChangeIndicator(AnalyticsType.WEEKLY_CHANGE);
+ bindValueToChangeIndicator(AnalyticsType.MONTHLY_CHANGE);
+ }
+
+ /**
+ * Updates the budget advice text based on the new value of the monthly spent.
+ * @param newValue the new value of the monthly spent
+ */
+ private void updateBudgetAdvice(Number newValue) {
+ String adviceText = "Great job! You are within your budget!";
+ if (analyticModel.getBudget() != 0 && newValue.doubleValue() > analyticModel.getBudget()) {
+ adviceText = "You have exceeded your monthly budget!";
+ }
+ budgetAdvice.setText(adviceText);
+ }
+
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof StatisticsPanel)) {
+ return false;
+ }
+
+ // state check
+ StatisticsPanel statistics = (StatisticsPanel) other;
+ return monthlySpending.getText().equals(statistics.monthlySpending.getText())
+ && monthlyRemaining.getText().equals(statistics.monthlyRemaining.getText())
+ && weeklySpending.getText().equals(statistics.weeklySpending.getText())
+ && weeklyRemaining.getText().equals(statistics.weeklyRemaining.getText())
+ && weeklyChange.getText().equals(statistics.weeklyChange.getText())
+ && monthlyChange.getText().equals(statistics.monthlyChange.getText())
+ && totalSpending.getText().equals(statistics.totalSpending.getText())
+ && budgetPercentage.getText().equals(statistics.budgetPercentage.getText());
+ }
+
+}
diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/fasttrack/ui/StatusBarFooter.java
similarity index 96%
rename from src/main/java/seedu/address/ui/StatusBarFooter.java
rename to src/main/java/fasttrack/ui/StatusBarFooter.java
index b577f829423..8a73e2edd31 100644
--- a/src/main/java/seedu/address/ui/StatusBarFooter.java
+++ b/src/main/java/fasttrack/ui/StatusBarFooter.java
@@ -1,4 +1,4 @@
-package seedu.address.ui;
+package fasttrack.ui;
import java.nio.file.Path;
import java.nio.file.Paths;
diff --git a/src/main/java/fasttrack/ui/SuggestionCard.java b/src/main/java/fasttrack/ui/SuggestionCard.java
new file mode 100644
index 00000000000..7a48badf31c
--- /dev/null
+++ b/src/main/java/fasttrack/ui/SuggestionCard.java
@@ -0,0 +1,49 @@
+package fasttrack.ui;
+
+import fasttrack.model.category.Category;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+/**
+ * A UI component that displays information of a {@code Category}.
+ */
+public class SuggestionCard extends UiPart {
+
+ private static final String FXML = "SuggestionListCard.fxml";
+
+
+ public final Category category;
+
+ @FXML
+ private HBox cardPane;
+ @FXML
+ private Label categoryName;
+
+ /**
+ * Creates a {@code CategoryCard} with the given {@code Category} and index to display.
+ */
+ public SuggestionCard(Category category) {
+ super(FXML);
+ this.category = category;
+ categoryName.setText(category.getCategoryName());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof SuggestionCard)) {
+ return false;
+ }
+
+ // state check
+ SuggestionCard card = (SuggestionCard) other;
+ return category.equals(card.category);
+ }
+}
diff --git a/src/main/java/fasttrack/ui/SuggestionListPanel.java b/src/main/java/fasttrack/ui/SuggestionListPanel.java
new file mode 100644
index 00000000000..4b4517f20e9
--- /dev/null
+++ b/src/main/java/fasttrack/ui/SuggestionListPanel.java
@@ -0,0 +1,177 @@
+package fasttrack.ui;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import fasttrack.model.category.Category;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.Region;
+
+/**
+ * Panel containing the list of expenses.
+ */
+public class SuggestionListPanel extends UiPart {
+ private static final String FXML = "SuggestionListPanel.fxml";
+
+ @FXML
+ private ListView suggestionListView;
+
+ private final FilteredList filteredCategoryList;
+ private final CommandBox commandBox;
+
+
+ /**
+ * Creates a {@code SuggestionListPanel} with the given {@code ObservableList}.
+ * @param categoryList the list of categories to use for the suggestions
+ * @param commandBox the CommandBox to use for autocomplete
+ */
+ public SuggestionListPanel(ObservableList