From 9491dffbda317f0752406ca15280316308ed4377 Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sat, 3 Aug 2024 12:58:29 +0530 Subject: [PATCH 01/14] Make header react to screen size --- ui/components/header.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/components/header.tsx b/ui/components/header.tsx index f69a198..42d5f7a 100644 --- a/ui/components/header.tsx +++ b/ui/components/header.tsx @@ -3,6 +3,7 @@ import { Avatar, Button, Divider, Flex, Input, InputRef, Select, Space, Typograp import React from "react"; import { connect } from "react-redux"; import { budgetSlice, headerSlice, navigation, store, View } from "../store"; +import { GetScreenSize, ScreenSize } from "../utils"; export interface HeaderBudgetDetails { name: string; @@ -23,8 +24,12 @@ interface HeaderState { class Header extends React.Component { render() { + let justify = "space-between"; + if (GetScreenSize() != ScreenSize.mobile) { + justify = "center"; + } return ( - + {this.render_budget_selector()} From 197d0573efd22dd6258f938cf048847d1605e0ef Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sat, 3 Aug 2024 15:22:02 +0530 Subject: [PATCH 02/14] Refactor form into components. --- ui/App.tsx | 4 +- ui/components/create_budget_form.tsx | 167 +++---------------------- ui/components/edit_categories_form.tsx | 94 ++++++++++++++ ui/pages/create_new_budget_page.tsx | 73 +++++++++++ ui/pages/no-budget_page.tsx | 16 --- ui/services/data_service.ts | 2 - 6 files changed, 186 insertions(+), 170 deletions(-) create mode 100644 ui/components/edit_categories_form.tsx create mode 100644 ui/pages/create_new_budget_page.tsx delete mode 100644 ui/pages/no-budget_page.tsx diff --git a/ui/App.tsx b/ui/App.tsx index 3fa254b..c4bd854 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -1,7 +1,7 @@ import { navigation, store, View } from './store' import { CreateDummyData } from './utils'; import { PingRemote } from './services/ping_service'; -import NoBudgetAvailablePage from './pages/no-budget_page'; +import CreateNewBudgetPage from './pages/create_new_budget_page'; import { ReactNode } from 'react'; import React from 'react'; import { connect } from 'react-redux'; @@ -91,7 +91,7 @@ class App extends React.Component { case View.Overview: return case View.NoBudgetAvailable: - return + return default: return (<>Not Found) } diff --git a/ui/components/create_budget_form.tsx b/ui/components/create_budget_form.tsx index b69453f..a227f75 100644 --- a/ui/components/create_budget_form.tsx +++ b/ui/components/create_budget_form.tsx @@ -7,68 +7,25 @@ import { CloseCircleOutlined, CloseOutlined, CloseSquareFilled, LoadingOutlined, import { Budget, Category, DataModelFactory } from "../datamodel/datamodel"; import { connect } from "react-redux"; -enum WizardStep { - CreateBudget, - AddCategories, - CreateReucrringExpenses, +export interface CreateBudgetFormProps { + onFinish: (budget: Budget) => void; + onCancel?: () => void; } -interface CreateBudgetPageState { - current_step: WizardStep; - is_loading: boolean; - budget_under_edit: null | Budget; -} - -interface CreateBudgetPageProps { - budget: Budget; -} - -class CreateBudgetForm extends React.Component { +class CreateBudgetForm extends React.Component { data_service: DataService; - getTitle(): string { - switch (this.state.current_step) { - case WizardStep.CreateBudget: - return "Create Budget"; - case WizardStep.AddCategories: - return "Add Categories"; - case WizardStep.CreateReucrringExpenses: - return "Create Recurring Expenses"; - default: - return "Create Budget"; - } - } - - constructor(props: CreateBudgetPageProps) { + constructor(props: CreateBudgetFormProps) { super(props); this.data_service = getDataService(); - this.state = { - current_step: WizardStep.CreateBudget, - is_loading: false, - budget_under_edit: null, - }; } render() { return ( - - {this.render_form()} - ); - } - render_form(): React.ReactNode { - if (this.state.is_loading) { - return (} size="large" />); - } - switch (this.state.current_step) { - case WizardStep.CreateBudget: - return this.render_create_budget(); - case WizardStep.AddCategories: - return this.render_add_category(); - case WizardStep.CreateReucrringExpenses: - return this.render_add_category(); - default: - return (<>); - } + <> + {this.render_create_budget()} + + ); } render_create_budget() { @@ -79,18 +36,15 @@ class CreateBudgetForm extends React.Component['onFinish'] = (values) => { this.setState({ is_loading: true }); this.data_service.createBudget(values.name!).then((value: Budget) => { - this.setState({ - is_loading: false, - current_step: WizardStep.AddCategories, - budget_under_edit: value, - }); + this.props.onFinish(value); }).catch((error) => { }); }; const render_cancel_button = () => { - if (this.props.budget != null) { - return () + if (this.props.onCancel != null) { + const onCancel = this.props.onCancel!; + return () } return (<>); } @@ -111,7 +65,9 @@ class CreateBudgetForm extends React.Component label="Description" name="description" - rules={[{ required: false }]}> + rules={[{ required: false }]}> + + - - - - - )} - - - ); - } - - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { - if (this.props.budget != null) { - this.setState({ - current_step: WizardStep.AddCategories, - budget_under_edit: this.props.budget, - }); - } - } - -} - -function mapStateToProps(state: any) { - const index = state.budget.selected_budget_index; - if (index == null) { - return { - budget: null, - }; - } else { - return { - budget: state.budget.budget_list[index], - }; - } } -export default connect(mapStateToProps)(CreateBudgetForm); \ No newline at end of file +export default CreateBudgetForm; \ No newline at end of file diff --git a/ui/components/edit_categories_form.tsx b/ui/components/edit_categories_form.tsx new file mode 100644 index 0000000..51b8c69 --- /dev/null +++ b/ui/components/edit_categories_form.tsx @@ -0,0 +1,94 @@ +import { Button, Card, Empty, Form, FormInstance, Input, InputNumber } from "antd"; +import React from "react"; +import { Budget, Category, DataModelFactory } from "../datamodel/datamodel"; +import { navigation, store, View } from "../store"; +import { CloseOutlined } from "@ant-design/icons"; +import { DataService, getDataService } from "../services/data_service"; + + +export interface EditCategoriesFormProps { + budget_id: string; + categories?: Category[]; + onCategoriesAdded: () => void; + onCancel: () => void; +} + +interface EditCategoriesFormState { + +} + +export class EditCategoriesForm extends React.Component { + data_service: DataService; + + constructor(props: EditCategoriesFormProps) { + super(props); + this.data_service = getDataService(); + } + + render() { + return ( + this.render_add_category() + ); + } + + render_add_category() { + const formRef = React.createRef(); + const onFinish = (values: any) => { + const category_list: Category[] = []; + for (const raw_category of values.categories) { + const category = DataModelFactory.createCategory(0); + category.name = raw_category.name; + category.allocation = raw_category.allocation; + category_list.push(category); + } + this.setState({ is_loading: true }); + this.data_service.createCategories(this.props.budget_id, category_list).then((value: Budget) => { + this.props.onCategoriesAdded(); + }).catch((_error: any) => { + }); + }; + return ( +
+ + {(fields, { add, remove }) => ( +
+ {fields.map((field) => ( + remove(field.name)} />]} + > + + + + + + + + ))} + {fields.length === 0 && ( + + )} + + + + + +
+ )} +
+
+ ); + } +} \ No newline at end of file diff --git a/ui/pages/create_new_budget_page.tsx b/ui/pages/create_new_budget_page.tsx new file mode 100644 index 0000000..b20d08c --- /dev/null +++ b/ui/pages/create_new_budget_page.tsx @@ -0,0 +1,73 @@ +import { Alert, Card, Flex, Spin } from 'antd'; +import React from 'react'; +import CreateBudgetForm from '../components/create_budget_form'; +import { Budget } from '../datamodel/datamodel'; +import { LoadingOutlined } from '@ant-design/icons'; +import { EditCategoriesForm } from '../components/edit_categories_form'; +import { navigation, store, View } from '../store'; + +enum WizardStep { + CreateBudget, + AddCategories, + CreateReucrringExpenses, +} + +interface CreateBudgetPageState { + current_step: WizardStep; + is_loading: boolean; + budget_under_edit: null | Budget; +} +export interface CreateBudgetPageProps { +} +export class CreateNewBudgetPage extends React.Component { + render() { + return ( + + + {this.render_form()} + + ); + } + + render_form(): React.ReactNode { + if (this.state.is_loading) { + return (} size="large" />); + } + switch (this.state.current_step) { + case WizardStep.CreateBudget: + return ( { this.setState({ current_step: WizardStep.AddCategories, budget_under_edit: budget }) }} />); + case WizardStep.AddCategories: + return (< EditCategoriesForm budget_id={this.state.budget_under_edit!.id} onCategoriesAdded={function (): void { + store.dispatch(navigation(View.Overview)); + }} onCancel={function (): void { + store.dispatch(navigation(View.Overview)); + }} />); + + default: + return (<>); + } + } + getTitle(): string { + switch (this.state.current_step) { + case WizardStep.CreateBudget: + return "Create Budget"; + case WizardStep.AddCategories: + return "Add Categories"; + case WizardStep.CreateReucrringExpenses: + return "Create Recurring Expenses"; + default: + return "Create Budget"; + } + } + + constructor(props: CreateBudgetPageProps) { + super(props); + this.state = { + current_step: WizardStep.CreateBudget, + is_loading: false, + budget_under_edit: null + }; + } +} + +export default CreateNewBudgetPage; \ No newline at end of file diff --git a/ui/pages/no-budget_page.tsx b/ui/pages/no-budget_page.tsx deleted file mode 100644 index 96033c3..0000000 --- a/ui/pages/no-budget_page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Alert, Card, Flex } from 'antd'; -import React from 'react'; -import CreateBudgetForm from '../components/create_budget_form'; - -export class NoBudgetAvailablePage extends React.Component { - render() { - return ( - <> - - - - ); - } -} - -export default NoBudgetAvailablePage; \ No newline at end of file diff --git a/ui/services/data_service.ts b/ui/services/data_service.ts index 8d15fba..a3e64c6 100644 --- a/ui/services/data_service.ts +++ b/ui/services/data_service.ts @@ -31,7 +31,6 @@ class RemoteDataService implements DataService { constructor() { this.BASE_URL = ""; - console.log(`Using remote data service at ${this.BASE_URL}`); } createBudget(name: string): Promise { @@ -132,7 +131,6 @@ class LocalDataService implements DataService { if (userData) { budgetList = JSON.parse(userData) ?? []; } - console.log(budgetList); resolve(budgetList); }); } From cd962f424837c1aacbcb422d96e976976ab68d8e Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sat, 3 Aug 2024 16:07:31 +0530 Subject: [PATCH 03/14] Fix dummy data generation --- .env.development | 2 +- ui/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.development b/.env.development index 1addb02..ba70415 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ -VITE_CREATE_DUMMY_DATA=false +VITE_CREATE_DUMMY_DATA=true VITE_PING_REMOTE=false VITE_USE_LOCAL_DATA_SERVICE=true VITE_CLEAR_USER_DATA_ON_LOAD=true \ No newline at end of file diff --git a/ui/utils.ts b/ui/utils.ts index e5b745e..1cbc202 100644 --- a/ui/utils.ts +++ b/ui/utils.ts @@ -94,5 +94,5 @@ export function CreateDummyData() { user_data.recurringList.push(recurring); } - return user_data; + return [user_data]; } \ No newline at end of file From ad2362254a945cce6295eb4c171d795668997544 Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sat, 3 Aug 2024 16:07:46 +0530 Subject: [PATCH 04/14] Remove commented code --- ui/pages/overview.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/pages/overview.tsx b/ui/pages/overview.tsx index 9fa2198..7a2a1b7 100644 --- a/ui/pages/overview.tsx +++ b/ui/pages/overview.tsx @@ -56,7 +56,6 @@ class OverviewPage extends React.Component { groupSeparator="" value={used_up_budget} precision={0} - // prefix={} suffix={"/ " + (this.state.total_allocations).toString()} valueStyle={{ color: status_color }} /> From ef50014816775c16c303fb20cb0bf0be7d376ba5 Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 06:17:48 +0530 Subject: [PATCH 05/14] Rename Edit Categories to Add Categories --- .../{edit_categories_form.tsx => add_categories_form.tsx} | 2 +- ui/pages/create_new_budget_page.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename ui/components/{edit_categories_form.tsx => add_categories_form.tsx} (97%) diff --git a/ui/components/edit_categories_form.tsx b/ui/components/add_categories_form.tsx similarity index 97% rename from ui/components/edit_categories_form.tsx rename to ui/components/add_categories_form.tsx index 51b8c69..b03e01d 100644 --- a/ui/components/edit_categories_form.tsx +++ b/ui/components/add_categories_form.tsx @@ -17,7 +17,7 @@ interface EditCategoriesFormState { } -export class EditCategoriesForm extends React.Component { +export class AddCategoriesForm extends React.Component { data_service: DataService; constructor(props: EditCategoriesFormProps) { diff --git a/ui/pages/create_new_budget_page.tsx b/ui/pages/create_new_budget_page.tsx index b20d08c..14d8823 100644 --- a/ui/pages/create_new_budget_page.tsx +++ b/ui/pages/create_new_budget_page.tsx @@ -3,7 +3,7 @@ import React from 'react'; import CreateBudgetForm from '../components/create_budget_form'; import { Budget } from '../datamodel/datamodel'; import { LoadingOutlined } from '@ant-design/icons'; -import { EditCategoriesForm } from '../components/edit_categories_form'; +import { AddCategoriesForm } from '../components/edit_categories_form'; import { navigation, store, View } from '../store'; enum WizardStep { @@ -37,7 +37,7 @@ export class CreateNewBudgetPage extends React.Component { this.setState({ current_step: WizardStep.AddCategories, budget_under_edit: budget }) }} />); case WizardStep.AddCategories: - return (< EditCategoriesForm budget_id={this.state.budget_under_edit!.id} onCategoriesAdded={function (): void { + return (< AddCategoriesForm budget_id={this.state.budget_under_edit!.id} onCategoriesAdded={function (): void { store.dispatch(navigation(View.Overview)); }} onCancel={function (): void { store.dispatch(navigation(View.Overview)); From b89d9ecc5dd462e81977c1c1de95a220cc546408 Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 08:26:23 +0530 Subject: [PATCH 06/14] Add edit categories layout --- ui/App.tsx | 10 ++++ ui/components/header.tsx | 3 +- ui/pages/create_new_budget_page.tsx | 2 +- ui/pages/edit_categories_page.tsx | 93 +++++++++++++++++++++++++++++ ui/services/data_service.ts | 40 +++++++++++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 ui/pages/edit_categories_page.tsx diff --git a/ui/App.tsx b/ui/App.tsx index c4bd854..ea2c8b4 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -8,6 +8,8 @@ import { connect } from 'react-redux'; import { ConfigProvider, Layout } from 'antd'; import Header from './components/header'; import Overview from './pages/overview'; +import EditCategoriesPage from './pages/edit_categories_page'; +import { Budget } from './datamodel/datamodel'; interface PreRun { @@ -19,6 +21,7 @@ interface PreRun { export interface AppProps { view: View; is_header_visible: boolean; + current_budget: Budget | null; } class App extends React.Component { @@ -92,15 +95,22 @@ class App extends React.Component { return case View.NoBudgetAvailable: return + case View.CategoryEdit: + return default: return (<>Not Found) } } } function mapStateToProps(state: any): AppProps { + let budget = null; + if (state.budget.budget_list && state.budget.selected_budget_index !== null) { + budget = state.budget.budget_list[state.budget.selected_budget_index]; + } return { view: state.navigation.current_view, is_header_visible: state.header.is_visible, + current_budget: budget } } diff --git a/ui/components/header.tsx b/ui/components/header.tsx index 42d5f7a..cb80b3b 100644 --- a/ui/components/header.tsx +++ b/ui/components/header.tsx @@ -1,4 +1,4 @@ -import { FileOutlined, MoreOutlined, PlusOutlined, SettingOutlined } from "@ant-design/icons"; +import { EditFilled, FileOutlined, MoreOutlined, PlusOutlined, SettingOutlined } from "@ant-design/icons"; import { Avatar, Button, Divider, Flex, Input, InputRef, Select, Space, Typography } from "antd"; import React from "react"; import { connect } from "react-redux"; @@ -31,6 +31,7 @@ class Header extends React.Component { return ( {this.render_budget_selector()} + ); diff --git a/ui/pages/create_new_budget_page.tsx b/ui/pages/create_new_budget_page.tsx index 14d8823..61dc80e 100644 --- a/ui/pages/create_new_budget_page.tsx +++ b/ui/pages/create_new_budget_page.tsx @@ -3,7 +3,7 @@ import React from 'react'; import CreateBudgetForm from '../components/create_budget_form'; import { Budget } from '../datamodel/datamodel'; import { LoadingOutlined } from '@ant-design/icons'; -import { AddCategoriesForm } from '../components/edit_categories_form'; +import { AddCategoriesForm } from '../components/add_categories_form'; import { navigation, store, View } from '../store'; enum WizardStep { diff --git a/ui/pages/edit_categories_page.tsx b/ui/pages/edit_categories_page.tsx new file mode 100644 index 0000000..576647f --- /dev/null +++ b/ui/pages/edit_categories_page.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { connect } from "react-redux"; +import { Budget } from "../datamodel/datamodel"; +import { budgetSlice, navigation, store, View } from "../store"; +import { Button, Card, Divider, Flex, Input, Popconfirm, Typography } from "antd"; +import { BackwardFilled, CheckOutlined, CloseOutlined, DeleteFilled, SendOutlined, UpOutlined } from "@ant-design/icons"; +import { DataService, getDataService } from "../services/data_service"; + + + +export interface EditCategoriesPageProps { + budget: Budget; +} + +interface EditCategoriesPageState { + +} + +class EditCategoriesPage extends React.Component { + _data_service: DataService; + + constructor(props: EditCategoriesPageProps) { + super(props); + this._data_service = getDataService(); + } + + render() { + return ( + + {this.render_control_buttons()} + {this.render_categories()} + + ); + } + + render_categories() { + return ( + + + < Divider /> + {this.props.budget.categoryList.map((category) => ( +
+ {this.render_catogory_list_row(category.id, category.name, category.allocation)} + < Divider /> +
+ ))} +
+
+ ); + } + + render_control_buttons() { + const on_back_click = () => { + store.dispatch(navigation(View.Overview)); + } + return ( + + + + + ) + } + + render_catogory_list_row(id: number, title: string, allocation: number) { + const delete_title = "Delete '" + title + "' category"; + const delete_question = "Are you sure to delete the category - " + title + "? All data will be lost."; + const on_delete_click = () => { + this._data_service.deleteCategory(this.props.budget.id, id) + .then((budget: Budget) => { store.dispatch(budgetSlice.actions.upadteBudget(budget)); }); + } + return ( +
+ + + + + + + + + +
+ ) + } +} + +export default EditCategoriesPage; \ No newline at end of file diff --git a/ui/services/data_service.ts b/ui/services/data_service.ts index a3e64c6..836d320 100644 --- a/ui/services/data_service.ts +++ b/ui/services/data_service.ts @@ -7,6 +7,7 @@ export interface DataService { getHistory(): Promise; createCategories(budget_id: string, categories: Category[]): Promise; updateCategory(budget_id: string, category: Category): Promise; + deleteCategory(budget_id: string, categoryId: number): Promise; updateExpense(budget_id: string, expense: Expense): Promise; deleteExpense(budget_id: string, expenseId: number): Promise; updateRecurring(budget_id: string, recurring: Recurring): Promise; @@ -33,6 +34,10 @@ class RemoteDataService implements DataService { this.BASE_URL = ""; } + deleteCategory(_budget_id: string, _categoryId: number): Promise { + throw new Error("Method not implemented."); + } + createBudget(name: string): Promise { const endpoint: string = `${this.BASE_URL}/api/Budget`; return fetch(endpoint, { @@ -112,6 +117,41 @@ class RemoteDataService implements DataService { } class LocalDataService implements DataService { + deleteCategory(budget_id: string, categoryId: number): Promise { + return new Promise((resolve, reject) => { + // Implement the logic to delete a category from local storage + // For example: + const userData = localStorage.getItem('userData'); + if (userData) { + let budget_list: Budget[] = JSON.parse(userData); + const index = this.find_budget_by_id(budget_id, budget_list); + if (index === -1) { + reject(); + } + let category: Category | null = null; + budget_list[index].categoryList = budget_list[index].categoryList.filter((c: Category) => { + if (c.id === categoryId) { + category = c; + return false; + } + return true; + }); + if (category) { + const user_action: UserAction = DataModelFactory.createUserAction(); + user_action.type = UserActionType.deleteCategory; + user_action.payload = category; + budget_list[index].userActions.push(user_action); + + localStorage.setItem('userData', JSON.stringify(budget_list)); + resolve(budget_list[index]); + } else { + reject(); + } + } else { + reject(); + } + }); + } find_budget_by_id(budget_id: string, budget_list: Budget[]): number { let found: number = -1; budget_list.forEach((b: Budget, index: number) => { From 7fad3f8d84db96e63bc880eadb2cebc889a2d8fa Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 10:43:47 +0530 Subject: [PATCH 07/14] Fix duplicate execution of pre_run --- ui/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/App.tsx b/ui/App.tsx index ea2c8b4..4d9b4e3 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -59,10 +59,10 @@ class App extends React.Component { view: View.Overview, is_header_visible: true } + this.run_pre_run(); } componentDidMount(): void { - this.run_pre_run(); } render(): ReactNode { @@ -104,7 +104,7 @@ class App extends React.Component { } function mapStateToProps(state: any): AppProps { let budget = null; - if (state.budget.budget_list && state.budget.selected_budget_index !== null) { + if (state.budget.budget_list && state.budget.selected_budget_index !== null && state.budget.budget_list.length > state.budget.selected_budget_index) { budget = state.budget.budget_list[state.budget.selected_budget_index]; } return { From bfaaa4d422fe6cc13c65cb51ab64869b7fa6b6b8 Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 10:44:09 +0530 Subject: [PATCH 08/14] Fix update action on budget slice --- ui/pages/edit_categories_page.tsx | 2 +- ui/pages/overview.tsx | 4 ++-- ui/store.ts | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/pages/edit_categories_page.tsx b/ui/pages/edit_categories_page.tsx index 576647f..506d495 100644 --- a/ui/pages/edit_categories_page.tsx +++ b/ui/pages/edit_categories_page.tsx @@ -66,7 +66,7 @@ class EditCategoriesPage extends React.Component { this._data_service.deleteCategory(this.props.budget.id, id) - .then((budget: Budget) => { store.dispatch(budgetSlice.actions.upadteBudget(budget)); }); + .then((budget: Budget) => { store.dispatch(budgetSlice.actions.updateCurrent(budget)); }); } return (
diff --git a/ui/pages/overview.tsx b/ui/pages/overview.tsx index 7a2a1b7..4da20f7 100644 --- a/ui/pages/overview.tsx +++ b/ui/pages/overview.tsx @@ -108,7 +108,7 @@ class OverviewPage extends React.Component { context!.processing = true; this.setState({ add_expense_mode_context: context }); this._data_service.updateExpense(budget.id, expense).then((data) => { - store.dispatch(budgetSlice.actions.upadteBudget({ budget: data })); + store.dispatch(budgetSlice.actions.updateCurrent({ budget: data })); this.setState({ add_expense_mode_context: null }); }).catch(() => this.setState({ add_expense_mode_context: null })); } else { @@ -266,7 +266,7 @@ class OverviewPage extends React.Component { this._data_service.getBudget().then((data) => { if (data.length > 0) { store.dispatch( - budgetSlice.actions.budget({ + budgetSlice.actions.set({ budget_list: data, selected_budget_index: 0 }) diff --git a/ui/store.ts b/ui/store.ts index 6b23c3d..ddd904e 100644 --- a/ui/store.ts +++ b/ui/store.ts @@ -43,15 +43,17 @@ export const budgetSlice = createSlice({ selected_budget_index: null, } as any, reducers: { - budget: (state, action) => { + set: (state, action) => { state.budget_list = action.payload.budget_list; state.selected_budget_index = action.payload.selected_budget_index; }, - upadteBudget: (state, action) => { + updateCurrent: (state, action) => { if (state.selected_budget_index == null) { return; } - state.budget_list[state.selected_budget_index] = action.payload.budget; + let budget_list = state.budget_list; + budget_list[state.selected_budget_index] = action.payload; + state.budget_list = budget_list; }, updateSelection: (state, action) => { const new_index = action.payload.index; From 583d92d94368d934bcfdb25545d4fd2e65e22d7b Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 14:05:50 +0530 Subject: [PATCH 09/14] Implement category edit buttons --- ui/pages/edit_categories_page.tsx | 50 ++++++++++++++++++++++--------- ui/pages/overview.tsx | 15 +++++++++- ui/services/data_service.ts | 6 +++- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/ui/pages/edit_categories_page.tsx b/ui/pages/edit_categories_page.tsx index 506d495..dc45a11 100644 --- a/ui/pages/edit_categories_page.tsx +++ b/ui/pages/edit_categories_page.tsx @@ -1,9 +1,9 @@ import React from "react"; import { connect } from "react-redux"; -import { Budget } from "../datamodel/datamodel"; +import { Budget, DataModelFactory } from "../datamodel/datamodel"; import { budgetSlice, navigation, store, View } from "../store"; import { Button, Card, Divider, Flex, Input, Popconfirm, Typography } from "antd"; -import { BackwardFilled, CheckOutlined, CloseOutlined, DeleteFilled, SendOutlined, UpOutlined } from "@ant-design/icons"; +import { BackwardFilled, CheckOutlined, CloseOutlined, DeleteFilled, LeftOutlined, SendOutlined, UpOutlined } from "@ant-design/icons"; import { DataService, getDataService } from "../services/data_service"; @@ -13,7 +13,7 @@ export interface EditCategoriesPageProps { } interface EditCategoriesPageState { - + is_loading: boolean; } class EditCategoriesPage extends React.Component { @@ -22,11 +22,14 @@ class EditCategoriesPage extends React.Component + {this.render_control_buttons()} {this.render_categories()} @@ -37,11 +40,9 @@ class EditCategoriesPage extends React.Component - < Divider /> {this.props.budget.categoryList.map((category) => (
{this.render_catogory_list_row(category.id, category.name, category.allocation)} - < Divider />
))}
@@ -54,9 +55,9 @@ class EditCategoriesPage extends React.Component - - + + + ) } @@ -64,16 +65,37 @@ class EditCategoriesPage extends React.Component { this._data_service.deleteCategory(this.props.budget.id, id) - .then((budget: Budget) => { store.dispatch(budgetSlice.actions.updateCurrent(budget)); }); + .then((budget: Budget) => store.dispatch(budgetSlice.actions.updateCurrent(budget))) + .finally(() => { this.setState({ is_loading: false }); }); + } + const on_title_change = (e: React.ChangeEvent) => { + updated_title = e.target.value; + } + const on_allocation_change = (e: React.ChangeEvent) => { + updated_allocation = parseInt(e.target.value); + } + const on_edit_click = () => { + let category = DataModelFactory.createCategory(0); + category.name = updated_title; + category.allocation = updated_allocation; + category.id = id; + this.setState({ is_loading: true }); + this._data_service.updateCategory(this.props.budget.id, category) + .then((budget: Budget) => store.dispatch(budgetSlice.actions.updateCurrent(budget))) + .finally(() => this.setState({ is_loading: false })); } return (
- - - + + + - +
diff --git a/ui/pages/overview.tsx b/ui/pages/overview.tsx index 4da20f7..c052d72 100644 --- a/ui/pages/overview.tsx +++ b/ui/pages/overview.tsx @@ -276,14 +276,21 @@ class OverviewPage extends React.Component { else { this.navigate_to(View.NoBudgetAvailable); } + }).catch((e) => { + console.error(e); + setTimeout(() => this.componentDidMount(), 1000); }); + this.update_calculations(); + this.update_next_recurring(); } componentDidUpdate(prevProps: Readonly, _prevState: Readonly, _snapshot?: any): void { if (this.props.selected_budget_index != null) { const current_budget = this.props.budget_list[this.props.selected_budget_index]; if (prevProps.selected_budget_index != this.props.selected_budget_index || - (prevProps.selected_budget_index != null && prevProps.budget_list[prevProps.selected_budget_index].last_updated != current_budget.last_updated)) { + (prevProps.selected_budget_index != null && + prevProps.budget_list[prevProps.selected_budget_index].last_updated != current_budget.last_updated) + ) { { this.update_calculations(); this.update_next_recurring(); @@ -297,6 +304,9 @@ class OverviewPage extends React.Component { } private update_next_recurring() { + if (this.props.selected_budget_index == null || this.props.budget_list == null) { + return; + } const budget = this.props.budget_list[this.props.selected_budget_index!]; const recurring_list: Recurring[] = budget.recurringList; const next_dates_ = recurring_list.map((recurring) => ( @@ -317,6 +327,9 @@ class OverviewPage extends React.Component { } private update_calculations() { + if (this.props.selected_budget_index == null || this.props.budget_list == null) { + return; + } const budget = this.props.budget_list[this.props.selected_budget_index!]; let total_allocations = 0; budget?.categoryList.forEach((category) => { diff --git a/ui/services/data_service.ts b/ui/services/data_service.ts index 836d320..c40ccd4 100644 --- a/ui/services/data_service.ts +++ b/ui/services/data_service.ts @@ -142,6 +142,7 @@ class LocalDataService implements DataService { user_action.payload = category; budget_list[index].userActions.push(user_action); + budget_list[index].last_updated = Date.now(); localStorage.setItem('userData', JSON.stringify(budget_list)); resolve(budget_list[index]); } else { @@ -233,6 +234,7 @@ class LocalDataService implements DataService { budget_list[index].userActions.push(user_action); } + budget_list[index].last_updated = Date.now(); localStorage.setItem('userData', JSON.stringify(budget_list)); resolve(budget_list[index]); } @@ -257,6 +259,7 @@ class LocalDataService implements DataService { budget_list[index].categoryList = parsedCategories.map((c: Category) => { if (c.id === category.id) { found = true; + category.expenseList = c.expenseList; return category; } return c; @@ -268,7 +271,7 @@ class LocalDataService implements DataService { user_action.type = UserActionType.updateCategory; user_action.payload = category; budget_list[index].userActions.push(user_action); - + budget_list[index].last_updated = Date.now(); localStorage.setItem('userData', JSON.stringify(budget_list)); resolve(budget_list[index]); }); @@ -345,6 +348,7 @@ class LocalDataService implements DataService { user_action.payload = expense; budget_list[index].userActions.push(user_action); + budget_list[index].last_updated = Date.now(); localStorage.setItem('userData', JSON.stringify(budget_list)); resolve(budget_list[index]); } else { From 4515f861b4362ac6ee0b62ca59e9c4ce2829a19a Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 14:25:46 +0530 Subject: [PATCH 10/14] Implement the backend APIs --- api/budget_controller.cs | 21 ++++++++++++++++++++- api/services/db_service.cs | 24 ++++++++++++++++++++++-- api/services/user_data_service.cs | 16 ++++++++++++++++ ui/services/data_service.ts | 29 ++++++++++++++++++++++++++--- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/api/budget_controller.cs b/api/budget_controller.cs index d85429f..cd7d981 100644 --- a/api/budget_controller.cs +++ b/api/budget_controller.cs @@ -28,12 +28,31 @@ public async Task CreateBudget([FromBody] CreateBudgetInput input return Ok(await _userDataService.CreateBudget(input.name)); } - [HttpPost("{budget_id}/categories")] + [HttpDelete("{budget_id}")] + public async Task DeleteBudget(string budget_id) + { + await _userDataService.DeleteBudget(budget_id); + return Ok(); + } + + [HttpPost("{budget_id}/add_categories")] public async Task AddCategoryInput(string budget_id, List categoryList) { return Ok(await _userDataService.AddCategoryToBudget(budget_id, categoryList)); } + [HttpPost("{budget_id}/update_category")] + public async Task UpdateCategoryInput(string budget_id, Category category) + { + return Ok(await _userDataService.UpdateCategory(budget_id, category)); + } + + [HttpDelete("{budget_id}/category/{category_id}")] + public async Task DeleteCategory(string budget_id, int category_id) + { + return Ok(await _userDataService.DeleteCategory(budget_id, category_id)); + } + [HttpPost("{budget_id}/expense")] public async Task AddExpenseInput(string budget_id, Expense expense) diff --git a/api/services/db_service.cs b/api/services/db_service.cs index e76273f..1897180 100644 --- a/api/services/db_service.cs +++ b/api/services/db_service.cs @@ -133,8 +133,16 @@ public async Task AddCategoryAsync(string budget_id, List categoryList public async Task UpdateCategoryAsync(string budget_id, Category category) { Budget budget = await GetBudgetAsync(budget_id); - budget.categoryList[budget.categoryList.FindIndex(c => c.Id == category.Id)] = category; - category.LastUpdated = DateTime.UtcNow.Ticks; + foreach (Category cat in budget.categoryList) + { + if (cat.Id == category.Id) + { + cat.Name = category.Name; + cat.Allocation = category.Allocation; + cat.LastUpdated = DateTime.UtcNow.Ticks; + break; + } + } await UpdateBudgetAsync(budget); } @@ -152,5 +160,17 @@ public async Task AddRecurringAsync(string budget_id, Recurring recurring) await UpdateBudgetAsync(budget); } + public async Task DeleteBudgetAsync(string budget_id) + { + await _container.DeleteItemAsync(budget_id, new PartitionKey(budget_id)); + } + + public async Task DeleteCategoryAsync(string budget_id, int category_id) + { + Budget budget = await GetBudgetAsync(budget_id); + budget.categoryList.RemoveAll(c => c.Id == category_id); + await UpdateBudgetAsync(budget); + return budget; + } } } \ No newline at end of file diff --git a/api/services/user_data_service.cs b/api/services/user_data_service.cs index 8523986..0bd9e83 100644 --- a/api/services/user_data_service.cs +++ b/api/services/user_data_service.cs @@ -46,4 +46,20 @@ public async Task AddCategoryToBudget(string budget_id, List c await _dbService.AddCategoryAsync(budget_id, categoryList); return await _dbService.GetBudgetAsync(budget_id); } + + public async Task UpdateCategory(string budget_id, Category category) + { + await _dbService.UpdateCategoryAsync(budget_id, category); + return await _dbService.GetBudgetAsync(budget_id); + } + + public async Task DeleteBudget(string budget_id) + { + await _dbService.DeleteBudgetAsync(budget_id); + } + + public async Task DeleteCategory(string budget_id, int category_id) + { + return await _dbService.DeleteCategoryAsync(budget_id, category_id); + } } \ No newline at end of file diff --git a/ui/services/data_service.ts b/ui/services/data_service.ts index c40ccd4..d05cd75 100644 --- a/ui/services/data_service.ts +++ b/ui/services/data_service.ts @@ -4,6 +4,7 @@ import { Category, Expense, Budget, BudgetHistory, Recurring, Unplanned, UserAct export interface DataService { getBudget(): Promise; createBudget(name: string): Promise; + deleteBudget(budget_id: string): Promise; getHistory(): Promise; createCategories(budget_id: string, categories: Category[]): Promise; updateCategory(budget_id: string, category: Category): Promise; @@ -33,9 +34,14 @@ class RemoteDataService implements DataService { constructor() { this.BASE_URL = ""; } + deleteBudget(budget_id: string): Promise { + const endpoint: string = `${this.BASE_URL}/api/Budget/${budget_id}`; + return fetch(endpoint, { method: 'DELETE' }).then(() => { }); + } deleteCategory(_budget_id: string, _categoryId: number): Promise { - throw new Error("Method not implemented."); + const endpoint: string = `${this.BASE_URL}/api/Budget/${_budget_id}/category/${_categoryId}`; + return fetch(endpoint, { method: 'DELETE' }).then((response) => response.json() as Promise); } createBudget(name: string): Promise { @@ -59,7 +65,7 @@ class RemoteDataService implements DataService { } updateCategory(budger_id: string, category: Category): Promise { - const endpoint: string = `${this.BASE_URL}/api/Budget/${budger_id}`; + const endpoint: string = `${this.BASE_URL}/api/Budget/${budger_id}/update_category`; return fetch(endpoint, { method: 'POST', body: JSON.stringify(category), @@ -70,7 +76,7 @@ class RemoteDataService implements DataService { } createCategories(_budget_id: string, _categories: Category[]): Promise { - const endpoint: string = `${this.BASE_URL}/api/Budget/${_budget_id}/categories`; + const endpoint: string = `${this.BASE_URL}/api/Budget/${_budget_id}/add_categories`; return fetch(endpoint, { method: 'POST', body: JSON.stringify(_categories), @@ -117,6 +123,23 @@ class RemoteDataService implements DataService { } class LocalDataService implements DataService { + deleteBudget(budget_id: string): Promise { + return new Promise((resolve, _reject) => { + // Implement the logic to delete a budget from local storage + // For example: + const userData = localStorage.getItem('userData'); + if (userData) { + let budget_list: Budget[] = JSON.parse(userData); + const index = this.find_budget_by_id(budget_id, budget_list); + if (index === -1) { + _reject(); + } + budget_list.splice(index, 1); + localStorage.setItem('userData', JSON.stringify(budget_list)); + resolve(); + } + }); + } deleteCategory(budget_id: string, categoryId: number): Promise { return new Promise((resolve, reject) => { // Implement the logic to delete a category from local storage From b3245b3c9062d178d145831a7360e50862dc74fe Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 14:43:32 +0530 Subject: [PATCH 11/14] Fixed delete flow --- api/services/db_service.cs | 3 +++ ui/pages/edit_categories_page.tsx | 14 +++++++++++++- ui/pages/overview.tsx | 8 ++++++-- ui/store.ts | 4 ++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/api/services/db_service.cs b/api/services/db_service.cs index 1897180..f3e66a6 100644 --- a/api/services/db_service.cs +++ b/api/services/db_service.cs @@ -162,6 +162,9 @@ public async Task AddRecurringAsync(string budget_id, Recurring recurring) public async Task DeleteBudgetAsync(string budget_id) { + var user_data = await GetUserData(_identityService.GetUserIdentity()); + user_data.BudgetIds.Remove(budget_id); + await UpdateUserData(user_data); await _container.DeleteItemAsync(budget_id, new PartitionKey(budget_id)); } diff --git a/ui/pages/edit_categories_page.tsx b/ui/pages/edit_categories_page.tsx index dc45a11..ecda5cc 100644 --- a/ui/pages/edit_categories_page.tsx +++ b/ui/pages/edit_categories_page.tsx @@ -57,7 +57,19 @@ class EditCategoriesPage extends React.Component - + { + this.setState({ is_loading: true }); this._data_service.deleteBudget(this.props.budget.id) + .then(() => store.dispatch(navigation(View.Overview))) + .finally(() => { this.setState({ is_loading: false }); }) + }} + > + + ) } diff --git a/ui/pages/overview.tsx b/ui/pages/overview.tsx index c052d72..381676d 100644 --- a/ui/pages/overview.tsx +++ b/ui/pages/overview.tsx @@ -263,6 +263,9 @@ class OverviewPage extends React.Component { } componentDidMount(): void { + store.dispatch(headerSlice.actions.header({ is_visible: false })); + store.dispatch(budgetSlice.actions.clear()); + this._data_service.getBudget().then((data) => { if (data.length > 0) { store.dispatch( @@ -272,6 +275,8 @@ class OverviewPage extends React.Component { }) ); store.dispatch(headerSlice.actions.header({ is_visible: true })); + this.update_calculations(); + this.update_next_recurring(); } else { this.navigate_to(View.NoBudgetAvailable); @@ -280,8 +285,7 @@ class OverviewPage extends React.Component { console.error(e); setTimeout(() => this.componentDidMount(), 1000); }); - this.update_calculations(); - this.update_next_recurring(); + } componentDidUpdate(prevProps: Readonly, _prevState: Readonly, _snapshot?: any): void { diff --git a/ui/store.ts b/ui/store.ts index ddd904e..ff36150 100644 --- a/ui/store.ts +++ b/ui/store.ts @@ -61,6 +61,10 @@ export const budgetSlice = createSlice({ return; } state.selected_budget_index = new_index; + }, + clear: (state) => { + state.budget_list = []; + state.selected_budget_index = null; } } }) From 87e071ad3ae5b2b56438fa9cb067912e44ced69b Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 15:10:08 +0530 Subject: [PATCH 12/14] Update header icon --- ui/components/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/header.tsx b/ui/components/header.tsx index cb80b3b..a2cfdeb 100644 --- a/ui/components/header.tsx +++ b/ui/components/header.tsx @@ -31,7 +31,7 @@ class Header extends React.Component { return ( {this.render_budget_selector()} - + ); From d3cf832b424b809990c052a336436b8938d267e8 Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 15:10:41 +0530 Subject: [PATCH 13/14] Integrate add category form to edit categories --- ui/pages/edit_categories_page.tsx | 37 +++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/ui/pages/edit_categories_page.tsx b/ui/pages/edit_categories_page.tsx index ecda5cc..dbe5bf8 100644 --- a/ui/pages/edit_categories_page.tsx +++ b/ui/pages/edit_categories_page.tsx @@ -3,8 +3,9 @@ import { connect } from "react-redux"; import { Budget, DataModelFactory } from "../datamodel/datamodel"; import { budgetSlice, navigation, store, View } from "../store"; import { Button, Card, Divider, Flex, Input, Popconfirm, Typography } from "antd"; -import { BackwardFilled, CheckOutlined, CloseOutlined, DeleteFilled, LeftOutlined, SendOutlined, UpOutlined } from "@ant-design/icons"; +import { BackwardFilled, CheckOutlined, CloseOutlined, DeleteFilled, LeftOutlined, PlusOutlined, SendOutlined, UpOutlined } from "@ant-design/icons"; import { DataService, getDataService } from "../services/data_service"; +import { AddCategoriesForm } from "../components/add_categories_form"; @@ -14,6 +15,7 @@ export interface EditCategoriesPageProps { interface EditCategoriesPageState { is_loading: boolean; + add_category_mode: boolean; } class EditCategoriesPage extends React.Component { @@ -24,6 +26,7 @@ class EditCategoriesPage extends React.Component {this.render_control_buttons()} - {this.render_categories()} + {this.render_body()} ); } + render_body() { + if (this.state.add_category_mode) { + return this.render_add_category(); + } else { + return this.render_categories(); + } + } + + render_add_category() { + return ( + { + store.dispatch(navigation(View.Overview)); + }} onCancel={() => { + this.setState({ add_category_mode: false }); + }} /> + ) + } + render_categories() { return ( @@ -52,11 +73,19 @@ class EditCategoriesPage extends React.Component { + if (this.state.add_category_mode) { + this.setState({ add_category_mode: false }); + return; + } store.dispatch(navigation(View.Overview)); } + const on_add_category_click = () => { + this.setState({ add_category_mode: true }); + } return ( - + + { this.setState({ is_loading: false }); }) }} > - + ) From ee125e0a89a8c0557e08c49b4eb82eb30549159c Mon Sep 17 00:00:00 2001 From: Sidhin S Thomas Date: Sun, 4 Aug 2024 15:10:52 +0530 Subject: [PATCH 14/14] Fix regression in adding expense --- ui/pages/overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/pages/overview.tsx b/ui/pages/overview.tsx index 381676d..172e658 100644 --- a/ui/pages/overview.tsx +++ b/ui/pages/overview.tsx @@ -108,7 +108,7 @@ class OverviewPage extends React.Component { context!.processing = true; this.setState({ add_expense_mode_context: context }); this._data_service.updateExpense(budget.id, expense).then((data) => { - store.dispatch(budgetSlice.actions.updateCurrent({ budget: data })); + store.dispatch(budgetSlice.actions.updateCurrent(data)); this.setState({ add_expense_mode_context: null }); }).catch(() => this.setState({ add_expense_mode_context: null })); } else {