├── .dockerignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── docker-image-ci-build.yml │ ├── docker-image-pr-build.yml │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── client ├── .docker │ └── nginx │ │ ├── conf │ │ ├── default.conf.template │ │ └── nginx-substitute.sh │ │ └── init-scripts │ │ └── 100-init-project-env-variables.sh ├── .gitignore ├── .yarnrc.yml ├── Dockerfile ├── README.md ├── eslint.config.js ├── index.html ├── package.json ├── postcss.config.cjs ├── public │ └── budgetBoardIcon.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── app │ │ ├── Authorized │ │ │ ├── Authorized.module.css │ │ │ ├── Authorized.tsx │ │ │ ├── Header │ │ │ │ ├── Header.module.css │ │ │ │ ├── Header.tsx │ │ │ │ └── SyncButton │ │ │ │ │ └── SyncButton.tsx │ │ │ ├── Navbar │ │ │ │ ├── Navbar.module.css │ │ │ │ ├── Navbar.tsx │ │ │ │ └── NavbarLink.tsx │ │ │ └── PageContent │ │ │ │ ├── Budgets │ │ │ │ ├── Budgets.module.css │ │ │ │ ├── Budgets.tsx │ │ │ │ ├── BudgetsContent │ │ │ │ │ ├── BudgetGroupHeader │ │ │ │ │ │ ├── BudgetsGroupHeader.module.css │ │ │ │ │ │ └── BudgetsGroupHeader.tsx │ │ │ │ │ ├── BudgetTotalCard │ │ │ │ │ │ ├── BudgetTotalCard.module.css │ │ │ │ │ │ ├── BudgetTotalCard.tsx │ │ │ │ │ │ └── BudgetTotalItem │ │ │ │ │ │ │ ├── BudgetTotalItem.module.css │ │ │ │ │ │ │ └── BudgetTotalItem.tsx │ │ │ │ │ ├── BudgetsContent.tsx │ │ │ │ │ ├── BudgetsGroup │ │ │ │ │ │ ├── BudgetParentCard │ │ │ │ │ │ │ ├── BudgetChildCard │ │ │ │ │ │ │ │ ├── BudgetChildCard.module.css │ │ │ │ │ │ │ │ └── BudgetChildCard.tsx │ │ │ │ │ │ │ ├── BudgetParentCard.module.css │ │ │ │ │ │ │ ├── BudgetParentCard.tsx │ │ │ │ │ │ │ └── UnbudgetChildCard │ │ │ │ │ │ │ │ ├── UnbudgetChildCard.module.css │ │ │ │ │ │ │ │ └── UnbudgetChildCard.tsx │ │ │ │ │ │ ├── BudgetsGroup.module.css │ │ │ │ │ │ └── BudgetsGroup.tsx │ │ │ │ │ ├── FixParentBudgetButton │ │ │ │ │ │ └── FixParentBudgetButton.tsx │ │ │ │ │ └── UnbudgetedGroup │ │ │ │ │ │ ├── UnbudgetedCard │ │ │ │ │ │ ├── UnbudgetedCard.module.css │ │ │ │ │ │ ├── UnbudgetedCard.tsx │ │ │ │ │ │ └── UnbudgetedChildCard │ │ │ │ │ │ │ ├── UnbudgetedChildCard.module.css │ │ │ │ │ │ │ └── UnbudgetedChildCard.tsx │ │ │ │ │ │ ├── UnbudgetedGroup.module.css │ │ │ │ │ │ └── UnbudgetedGroup.tsx │ │ │ │ └── BudgetsToolbar │ │ │ │ │ ├── AddBudget │ │ │ │ │ ├── AddBudget.module.css │ │ │ │ │ └── AddBudget.tsx │ │ │ │ │ └── BudgetsToolbar.tsx │ │ │ │ ├── Dashboard │ │ │ │ ├── AccountsCard │ │ │ │ │ ├── AccountsCard.module.css │ │ │ │ │ ├── AccountsCard.tsx │ │ │ │ │ ├── AccountsSettings │ │ │ │ │ │ ├── AccountsSettings.module.css │ │ │ │ │ │ ├── AccountsSettings.tsx │ │ │ │ │ │ ├── AccountsSettingsModal │ │ │ │ │ │ │ ├── AccountsSettingsModal.module.css │ │ │ │ │ │ │ ├── AccountsSettingsModal.tsx │ │ │ │ │ │ │ ├── DeletedAccounts │ │ │ │ │ │ │ │ ├── DeletedAccountCard │ │ │ │ │ │ │ │ │ ├── DeletedAccountCard.module.css │ │ │ │ │ │ │ │ │ └── DeletedAccountCard.tsx │ │ │ │ │ │ │ │ └── DeletedAccounts.tsx │ │ │ │ │ │ │ └── InstitutionSettingsCard │ │ │ │ │ │ │ │ ├── AccountSettingsCard │ │ │ │ │ │ │ │ ├── AccountSettingsCard.tsx │ │ │ │ │ │ │ │ └── DeleteAccountPopover │ │ │ │ │ │ │ │ │ └── DeleteAccountPopover.tsx │ │ │ │ │ │ │ │ ├── InstitutionSettingsCard.module.css │ │ │ │ │ │ │ │ └── InstitutionSettingsCard.tsx │ │ │ │ │ │ └── CreateAccountModal │ │ │ │ │ │ │ └── CreateAccountModal.tsx │ │ │ │ │ └── InstitutionItems │ │ │ │ │ │ ├── AccountItem │ │ │ │ │ │ └── AccountItem.tsx │ │ │ │ │ │ ├── InstitutionItem.module.css │ │ │ │ │ │ └── InstitutionItem.tsx │ │ │ │ ├── Dashboard.module.css │ │ │ │ ├── Dashboard.tsx │ │ │ │ ├── Footer │ │ │ │ │ ├── Footer.module.css │ │ │ │ │ └── Footer.tsx │ │ │ │ ├── NetWorthCard │ │ │ │ │ ├── NetWorthCard.module.css │ │ │ │ │ ├── NetWorthCard.tsx │ │ │ │ │ └── NetWorthItem │ │ │ │ │ │ ├── NetWorthItem.module.css │ │ │ │ │ │ └── NetWorthItem.tsx │ │ │ │ ├── SpendingTrendsCard │ │ │ │ │ ├── SpendingTrendsCard.module.css │ │ │ │ │ └── SpendingTrendsCard.tsx │ │ │ │ └── UncategorizedTransactionsCard │ │ │ │ │ ├── UncategorizedTransaction │ │ │ │ │ ├── UncategorizedTransaction.module.css │ │ │ │ │ └── UncategorizedTransaction.tsx │ │ │ │ │ ├── UncategorizedTransactionsCard.module.css │ │ │ │ │ └── UncategorizedTransactionsCard.tsx │ │ │ │ ├── Goals │ │ │ │ ├── CompletedGoalsAccordion │ │ │ │ │ ├── CompletedGoalCard │ │ │ │ │ │ └── CompletedGoalCard.tsx │ │ │ │ │ └── CompletedGoalsAccordion.tsx │ │ │ │ ├── GoalCard │ │ │ │ │ ├── EditableGoalMonthlyAmountCell │ │ │ │ │ │ ├── EditableGoalMonthlyAmountCell.module.css │ │ │ │ │ │ └── EditableGoalMonthlyAmountCell.tsx │ │ │ │ │ ├── EditableGoalNameCell │ │ │ │ │ │ ├── EditableGoalNameCell.module.css │ │ │ │ │ │ └── EditableGoalNameCell.tsx │ │ │ │ │ ├── EditableGoalTargetAmountCell │ │ │ │ │ │ ├── EditableGoalTargetAmountCell.module.css │ │ │ │ │ │ └── EditableGoalTargetAmountCell.tsx │ │ │ │ │ ├── EditableGoalTargetDateCell │ │ │ │ │ │ ├── EditableGoalTargetDateCell.module.css │ │ │ │ │ │ └── EditableGoalTargetDateCell.tsx │ │ │ │ │ ├── GoalCard.module.css │ │ │ │ │ ├── GoalCard.tsx │ │ │ │ │ └── GoalDetails │ │ │ │ │ │ └── GoalDetails.tsx │ │ │ │ ├── Goals.module.css │ │ │ │ ├── Goals.tsx │ │ │ │ └── GoalsHeader │ │ │ │ │ ├── AddGoalModal │ │ │ │ │ ├── AddGoalModal.tsx │ │ │ │ │ ├── PayGoalForm │ │ │ │ │ │ └── PayGoalForm.tsx │ │ │ │ │ └── SaveGoalForm │ │ │ │ │ │ └── SaveGoalForm.tsx │ │ │ │ │ ├── GoalsHeader.module.css │ │ │ │ │ └── GoalsHeader.tsx │ │ │ │ ├── PageContent.module.css │ │ │ │ ├── PageContent.tsx │ │ │ │ ├── Settings │ │ │ │ ├── DarkModeToggle.tsx │ │ │ │ ├── LinkSimpleFin.tsx │ │ │ │ ├── ResetPassword.tsx │ │ │ │ ├── Settings.module.css │ │ │ │ ├── Settings.tsx │ │ │ │ └── UserSettings.tsx │ │ │ │ ├── Transactions │ │ │ │ ├── TransactionCards.tsx │ │ │ │ │ ├── TransactionCard │ │ │ │ │ │ ├── EditableCategoryCell │ │ │ │ │ │ │ ├── EditableCategoryCell.module.css │ │ │ │ │ │ │ └── EditableCategoryCell.tsx │ │ │ │ │ │ ├── SelectedTransactionCard │ │ │ │ │ │ │ └── SelectedTransactionCard.tsx │ │ │ │ │ │ ├── SplitTransaction │ │ │ │ │ │ │ └── SplitTransaction.tsx │ │ │ │ │ │ ├── TransactionCard.module.css │ │ │ │ │ │ ├── TransactionCard.tsx │ │ │ │ │ │ └── UnselectedTransactionCard │ │ │ │ │ │ │ └── UnselectedTransactionCard.tsx │ │ │ │ │ └── TransactionCards.tsx │ │ │ │ ├── Transactions.module.css │ │ │ │ ├── Transactions.tsx │ │ │ │ └── TransactionsHeader │ │ │ │ │ ├── CreateTransactionModal │ │ │ │ │ └── CreateTransactionModal.tsx │ │ │ │ │ ├── FilterCard │ │ │ │ │ ├── FilterCard.module.css │ │ │ │ │ └── FilterCard.tsx │ │ │ │ │ ├── ImportTransactionsModal │ │ │ │ │ ├── AccountMapping │ │ │ │ │ │ └── AccountMapping.tsx │ │ │ │ │ ├── ColumnsOptions │ │ │ │ │ │ └── ColumnsOptions.tsx │ │ │ │ │ ├── ColumnsSelect │ │ │ │ │ │ └── ColumnsSelect.tsx │ │ │ │ │ ├── CsvOptions │ │ │ │ │ │ └── CsvOptions.tsx │ │ │ │ │ ├── DuplicateTransactionTable │ │ │ │ │ │ └── DuplicateTransactionTable.tsx │ │ │ │ │ ├── ImportTransactionsModal.tsx │ │ │ │ │ └── TransactionsTable │ │ │ │ │ │ └── TransactionsTable.tsx │ │ │ │ │ ├── SortMenu │ │ │ │ │ ├── SortMenu.module.css │ │ │ │ │ ├── SortMenu.tsx │ │ │ │ │ └── SortMenuHelpers.ts │ │ │ │ │ ├── TransactionsHeader.module.css │ │ │ │ │ ├── TransactionsHeader.tsx │ │ │ │ │ └── TransactionsSettings │ │ │ │ │ ├── CustomCategories │ │ │ │ │ ├── AddCategory │ │ │ │ │ │ └── AddCategory.tsx │ │ │ │ │ ├── CustomCategories.module.css │ │ │ │ │ ├── CustomCategories.tsx │ │ │ │ │ └── CustomCategoryCard │ │ │ │ │ │ ├── CustomCategoryCard.module.css │ │ │ │ │ │ └── CustomCategoryCard.tsx │ │ │ │ │ ├── DeletedTransactionCard │ │ │ │ │ ├── DeletedTransactionsCard.module.css │ │ │ │ │ └── DeletedTransactionsCard.tsx │ │ │ │ │ └── TransactionsSettings.tsx │ │ │ │ └── Trends │ │ │ │ ├── AssetsTab │ │ │ │ ├── AssetsTab.module.css │ │ │ │ └── AssetsTab.tsx │ │ │ │ ├── LiabilitiesTab │ │ │ │ ├── LiabilitiesTab.module.css │ │ │ │ └── LiabilitiesTab.tsx │ │ │ │ ├── NetCashFlowTab │ │ │ │ ├── NetCashFlowTab.module.css │ │ │ │ └── NetCashFlowTab.tsx │ │ │ │ ├── NetWorthTab │ │ │ │ ├── NetWorthTab.module.css │ │ │ │ └── NetWorthTab.tsx │ │ │ │ ├── SpendingTab │ │ │ │ ├── SpendingTab.module.css │ │ │ │ └── SpendingTab.tsx │ │ │ │ ├── Trends.module.css │ │ │ │ └── Trends.tsx │ │ └── Unauthorized │ │ │ ├── Login.tsx │ │ │ ├── Register.tsx │ │ │ ├── ResetPassword.tsx │ │ │ ├── Welcome.module.css │ │ │ └── Welcome.tsx │ ├── assets │ │ ├── budget-board-logo.tsx │ │ └── budgetBoardLogo.svg │ ├── components │ │ ├── AccountSelectInput.tsx │ │ ├── AccountsSelectHeader │ │ │ └── AccountsSelectHeader.tsx │ │ ├── AuthProvider │ │ │ ├── AuthProvider.tsx │ │ │ ├── AuthorizedRoute.tsx │ │ │ └── UnauthorizedRoute.tsx │ │ ├── CategorySelect.tsx │ │ ├── Charts │ │ │ ├── BalanceChart │ │ │ │ └── BalanceChart.tsx │ │ │ ├── NetCashFlowChart │ │ │ │ └── NetCashFlowChart.tsx │ │ │ ├── NetWorthChart │ │ │ │ └── NetWorthChart.tsx │ │ │ └── SpendingChart │ │ │ │ └── SpendingChart.tsx │ │ ├── MonthToolcards │ │ │ ├── MonthToolcard │ │ │ │ ├── MonthToolcard.module.css │ │ │ │ └── MonthToolcard.tsx │ │ │ ├── MonthToolcards.module.css │ │ │ └── MonthToolcards.tsx │ │ ├── SortButton.tsx │ │ └── Sortable │ │ │ ├── Sortable.tsx │ │ │ ├── SortableHandle.tsx │ │ │ └── SortableItem.tsx │ ├── helpers │ │ ├── accounts.ts │ │ ├── balances.ts │ │ ├── budgets.ts │ │ ├── category.ts │ │ ├── charts.ts │ │ ├── currency.ts │ │ ├── datetime.ts │ │ ├── goals.ts │ │ ├── requests.ts │ │ ├── transactions.ts │ │ └── utils.ts │ ├── index.css │ ├── main.tsx │ ├── models │ │ ├── account.ts │ │ ├── applicationUser.ts │ │ ├── balance.ts │ │ ├── budget.ts │ │ ├── category.ts │ │ ├── goal.ts │ │ ├── institution.ts │ │ ├── transaction.ts │ │ └── userSettings.ts │ ├── shared │ │ └── projectEnvVariables.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── compose.override.yml ├── compose.yml ├── img ├── budget-board-budgets.png ├── budget-board-dashboard.png └── logo.svg └── server ├── .gitignore ├── BudgetBoard.Database ├── BudgetBoard.Database.csproj ├── Data │ └── UserDataContext.cs ├── Migrations │ ├── 20231218061703_InitialDatabase.Designer.cs │ ├── 20231218061703_InitialDatabase.cs │ ├── 20240114181918_ChangeAmountToDecimal.Designer.cs │ ├── 20240114181918_ChangeAmountToDecimal.cs │ ├── 20240120050640_UpdateTypeType.Designer.cs │ ├── 20240120050640_UpdateTypeType.cs │ ├── 20240123044246_AddSubCategory.Designer.cs │ ├── 20240123044246_AddSubCategory.cs │ ├── 20240124031230_ChangeToOneWord.Designer.cs │ ├── 20240124031230_ChangeToOneWord.cs │ ├── 20240217002101_AddAccessToken.Designer.cs │ ├── 20240217002101_AddAccessToken.cs │ ├── 20240316022816_Add-Sync-ID.Designer.cs │ ├── 20240316022816_Add-Sync-ID.cs │ ├── 20240316042033_Add-Sync-ID-account.Designer.cs │ ├── 20240316042033_Add-Sync-ID-account.cs │ ├── 20240316195318_Add-Account-CurrentBalance.Designer.cs │ ├── 20240316195318_Add-Account-CurrentBalance.cs │ ├── 20240317163328_Add-LastSync.Designer.cs │ ├── 20240317163328_Add-LastSync.cs │ ├── 20240331223918_AddBudgets.Designer.cs │ ├── 20240331223918_AddBudgets.cs │ ├── 20240331225133_AddBudgetsFix.Designer.cs │ ├── 20240331225133_AddBudgetsFix.cs │ ├── 20240401020645_AddBudgetsDeleted.Designer.cs │ ├── 20240401020645_AddBudgetsDeleted.cs │ ├── 20240401040512_FixBudget.Designer.cs │ ├── 20240401040512_FixBudget.cs │ ├── 20240409004620_AddHideTransactionBool.Designer.cs │ ├── 20240409004620_AddHideTransactionBool.cs │ ├── 20240502020354_AddHideAccount.Designer.cs │ ├── 20240502020354_AddHideAccount.cs │ ├── 20240506015343_AddDeleted.Designer.cs │ ├── 20240506015343_AddDeleted.cs │ ├── 20240519182542_AddGoals.Designer.cs │ ├── 20240519182542_AddGoals.cs │ ├── 20240519204749_AddCategory.Designer.cs │ ├── 20240519204749_AddCategory.cs │ ├── 20240520034903_UpdateGoal.Designer.cs │ ├── 20240520034903_UpdateGoal.cs │ ├── 20240524040826_UpdateGoalMonthlyContribution.Designer.cs │ ├── 20240524040826_UpdateGoalMonthlyContribution.cs │ ├── 20240525030443_UpdateGoalParams.Designer.cs │ ├── 20240525030443_UpdateGoalParams.cs │ ├── 20240601035923_AddIdentity.Designer.cs │ ├── 20240601035923_AddIdentity.cs │ ├── 20240821025811_AddBalanceDate.Designer.cs │ ├── 20240821025811_AddBalanceDate.cs │ ├── 20241119004148_AddBalances.Designer.cs │ ├── 20241119004148_AddBalances.cs │ ├── 20241213043817_AddInstitution.Designer.cs │ ├── 20241213043817_AddInstitution.cs │ ├── 20241214190502_AddAccountIndex.Designer.cs │ ├── 20241214190502_AddAccountIndex.cs │ ├── 20241223045954_RemoveAccountBalanceProperties.Designer.cs │ ├── 20241223045954_RemoveAccountBalanceProperties.cs │ ├── 20241224001212_UpdateBudgetFloatToDecimal.Designer.cs │ ├── 20241224001212_UpdateBudgetFloatToDecimal.cs │ ├── 20250105233617_AddTransactionCategory.Designer.cs │ ├── 20250105233617_AddTransactionCategory.cs │ ├── 20250106051133_DropDeleted.Designer.cs │ ├── 20250106051133_DropDeleted.cs │ ├── 20250109054806_RemoveNullable.Designer.cs │ ├── 20250109054806_RemoveNullable.cs │ ├── 20250222202242_RemoveUserUid.Designer.cs │ ├── 20250222202242_RemoveUserUid.cs │ ├── 20250322013118_AddDeletedToInstitution.Designer.cs │ ├── 20250322013118_AddDeletedToInstitution.cs │ ├── 20250404235803_AddAccountSource.Designer.cs │ ├── 20250404235803_AddAccountSource.cs │ ├── 20250415230451_AddGoalCompleted.Designer.cs │ ├── 20250415230451_AddGoalCompleted.cs │ ├── 20250523225400_AddInterestRate.Designer.cs │ ├── 20250523225400_AddInterestRate.cs │ ├── 20250528000456_AddUserSettings.Designer.cs │ ├── 20250528000456_AddUserSettings.cs │ └── UserDataContextModelSnapshot.cs └── Models │ ├── Account.cs │ ├── ApplicationUser.cs │ ├── Balance.cs │ ├── Budget.cs │ ├── Category.cs │ ├── Goal.cs │ ├── Institution.cs │ ├── Transaction.cs │ └── UserSettings.cs ├── BudgetBoard.Service ├── AccountService.cs ├── ApplicationUserService.cs ├── BalanceService.cs ├── BudgetBoard.Service.csproj ├── BudgetService.cs ├── GoalService.cs ├── Helpers │ └── TransactionCategoriesHelpers.cs ├── InsitutionService.cs ├── Interfaces │ ├── IAccountService.cs │ ├── IApplicationUserService.cs │ ├── IBalanceService.cs │ ├── IBudgetService.cs │ ├── IGoalService.cs │ ├── IInstitutionService.cs │ ├── ISimpleFinService.cs │ ├── ITransactionCategoryService.cs │ ├── ITransactionService.cs │ └── IUserSettingsService.cs ├── Models │ ├── Account.cs │ ├── ApplicationUser.cs │ ├── Balance.cs │ ├── Budget.cs │ ├── BudgetBoardServiceException.cs │ ├── Category.cs │ ├── Goal.cs │ ├── Institution.cs │ ├── SimpleFin.cs │ ├── Transaction.cs │ ├── TransactionCategoriesConstants.cs │ └── UserSettings.cs ├── SimpleFinService.cs ├── TransactionCategoryService.cs ├── TransactionService.cs └── UserSettingsService.cs ├── BudgetBoard.Tests ├── AccountServiceTests.cs ├── ApplicationUserTests.cs ├── BalanceServiceTests.cs ├── BudgetBoard.IntegrationTests.csproj ├── BudgetServiceTests.cs ├── Fakers │ ├── AccountFaker.cs │ ├── ApplicationUserFaker.cs │ ├── BalanceFaker.cs │ ├── BudgetFaker.cs │ ├── GoalFaker.cs │ ├── InstitutionFaker.cs │ ├── TransactionCategoryFaker.cs │ ├── TransactionFaker.cs │ └── UserSettingsFaker.cs ├── GlobalUsings.cs ├── GoalServiceTests.cs ├── InstitutionServiceTests.cs ├── SimpleFinServiceTests.cs ├── TestHelper.cs ├── TransactionCategoryServiceTests.cs ├── TransactionServiceTests.cs └── UserSettingsServiceTests.cs ├── BudgetBoard.WebAPI ├── BudgetBoard.WebAPI.csproj ├── Controllers │ ├── AccountController.cs │ ├── ApplicationUserController.cs │ ├── BalanceController.cs │ ├── BudgetController.cs │ ├── GoalController.cs │ ├── InstitutionController.cs │ ├── SimpleFinController.cs │ ├── TransactionCategoryController.cs │ ├── TransactionController.cs │ └── UserSettingsController.cs ├── Jobs │ └── SyncBackgroundJob.cs ├── Overrides │ ├── IdentityApiEndpointRouteBuilderExtensions.cs │ └── IdentityApiEndpointRouteBuilderOptions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Utils │ ├── EmailSender.cs │ └── Helpers.cs ├── appsettings.Development.json ├── appsettings.json └── libman.json ├── BudgetBoard.sln ├── Dockerfile └── editorconfig /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/bin 15 | **/charts 16 | **/docker-compose* 17 | **/compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: teelur 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-pr-build.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Images 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | dockerfile: 13 | - ./client 14 | - ./server 15 | include: 16 | - dockerfile: ./client 17 | image: ghcr.io/teelur/budget-board/client 18 | - dockerfile: ./server 19 | image: ghcr.io/teelur/budget-board/server 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | 31 | - name: Extract metadata (tags, labels) for Docker 32 | id: meta 33 | uses: docker/metadata-action@v5 34 | with: 35 | images: ${{ matrix.image }} 36 | 37 | - name: Build the Docker image 38 | id: push 39 | uses: docker/build-push-action@v6 40 | with: 41 | context: ${{ matrix.dockerfile }} 42 | push: false 43 | platforms: linux/amd64,linux/arm64 44 | tags: ${{ steps.meta.outputs.tags }} 45 | labels: ${{ steps.meta.outputs.labels }} 46 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Dotnet Unit Tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test-dotnet: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup dotnet 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: "9.0.x" 20 | 21 | - name: Install dependencies 22 | working-directory: ./server 23 | run: dotnet restore 24 | 25 | - name: Build 26 | working-directory: ./server 27 | run: dotnet build --no-restore 28 | 29 | - name: Test with the dotnet CLI 30 | working-directory: ./server 31 | run: dotnet test --no-build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # Excluded directories/files 21 | .vscode 22 | **/db.sqlite3 23 | **/__pycache__/ 24 | .env* 25 | **/node_modules/ 26 | nginx.conf 27 | 28 | # Yarn 29 | client/.yarn/install-state.gz 30 | 31 | # Distribution / packaging 32 | .Python 33 | env/ 34 | build/ 35 | 36 | # ========================= 37 | # Operating System Files 38 | # ========================= 39 | 40 | # OSX 41 | # ========================= 42 | 43 | .DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | 58 | # Directories potentially created on remote AFP share 59 | .AppleDB 60 | .AppleDesktop 61 | Network Trash Folder 62 | Temporary Items 63 | .apdisk 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Budget Board 3 |
4 | 5 | --- 6 | 7 | [![Build and Publish](https://github.com/teelur/budget-board/actions/workflows/docker-image-ci-build.yml/badge.svg)](https://github.com/teelur/budget-board/actions/workflows/docker-image-ci-build.yml) 8 | ![GitHub Release](https://img.shields.io/github/v/release/teelur/budget-board) 9 | 10 | A simple app for tracking monthly spending and working towards financial goals. 11 | 12 | ## About The Project 13 | 14 | I created this app to be a self-hosted alternative to the now-shut-down personal finance app Mint. 15 | 16 | ### Features 17 | 18 | - Automatically sync your bank account data with [SimpleFIN](https://www.simplefin.org/) 19 | - Dashboard to view account data at a glance 20 | - Organize transactions into categories and subcategories 21 | - Create monthly budgets for categories and subcategories 22 | - Create and track goals for savings/loan payoff 23 | - Graphs to view trends over time (Spending, Assets, Liabilities, Net Worth, and more) 24 | 25 | ### Feedback 26 | 27 | - [Notice a bug?](https://github.com/teelur/budget-board/issues/new/choose) 28 | - [Have a feature request?](https://github.com/teelur/budget-board/discussions/categories/feature-requests) 29 | - [Have any feedback?](https://github.com/teelur/budget-board/discussions/categories/feedback) 30 | - [Have a question?](https://github.com/teelur/budget-board/discussions/categories/q-a) 31 | 32 | ### Screenshots 33 | 34 | dash 35 | dash 36 | 37 | ## Configuration 38 | 39 | ### Setting up Docker Compose 40 | 41 | This project is deployed using Docker Compose. 42 | 43 | The `compose.yml` and `compose.override.yml` files are used to deploy the app. 44 | Both files are able to deploy the app as is, but it is recommended to at least update the database password. 45 | 46 | See the [wiki](https://github.com/teelur/budget-board/wiki/Deploying-via-Docker-Compose) for more details about configuring the compose override file. 47 | 48 | ### Deploy 49 | 50 | Deploy the app by running the following command: 51 | 52 | ``` 53 | docker compose up -d 54 | ``` 55 | 56 | You can now access the app at `localhost:6253`. 57 | 58 | Check out the [wiki](https://github.com/teelur/budget-board/wiki) for more details about configuration and using the app :) 59 | -------------------------------------------------------------------------------- /client/.docker/nginx/conf/default.conf.template: -------------------------------------------------------------------------------- 1 | upstream backend_server { 2 | # This should be the name of the server container and it's configured port 3 | server budget-board-server:8080; 4 | } 5 | 6 | server { 7 | listen ${PORT}; 8 | 9 | # You should configure this to the domain that points to this app. 10 | server_name ${VITE_API_URL}; 11 | 12 | access_log /var/log/nginx/nginx.vhost.access.log; 13 | error_log /var/log/nginx/nginx.vhost.error.log; 14 | 15 | location / { 16 | # This would be the directory where your React app's static files are stored. 17 | root /usr/share/nginx/html; 18 | try_files ${DOLLAR}uri /index.html; 19 | } 20 | 21 | location /api/ { 22 | # This will redirect a request from this container to our backend container. 23 | proxy_pass http://backend_server/api/; 24 | proxy_set_header Host ${DOLLAR}proxy_host:${DOLLAR}proxy_port; 25 | # These are used to identify the original reverse proxied request 26 | proxy_set_header X-Forwarded-For ${DOLLAR}proxy_add_x_forwarded_for; 27 | proxy_set_header X-Forwarded-Host ${DOLLAR}http_host; 28 | proxy_set_header X-Forwarded-Proto ${DOLLAR}scheme; 29 | } 30 | } -------------------------------------------------------------------------------- /client/.docker/nginx/conf/nginx-substitute.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Make sure your end of line is LF. Otherwise this will cause issues. 3 | export DOLLAR="$" 4 | envsubst < default.conf.template > /etc/nginx/conf.d/default.conf 5 | nginx -g "daemon off;" -------------------------------------------------------------------------------- /client/.docker/nginx/init-scripts/100-init-project-env-variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | # Find the file where environment variables need to be replaced. 6 | projectEnvVariables=$(ls -t /usr/share/nginx/html/assets/projectEnvVariables*.js | head -n1) 7 | 8 | # Replace environment variables 9 | envsubst < "$projectEnvVariables" > ./projectEnvVariables_temp 10 | cp ./projectEnvVariables_temp "$projectEnvVariables" 11 | rm ./projectEnvVariables_temp -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | *.tsbuildinfo 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /client/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23 AS build-stage 2 | 3 | WORKDIR /app 4 | 5 | RUN corepack enable 6 | 7 | COPY ["./package.json", "./yarn.lock", "./.yarnrc.yml", "./"] 8 | RUN yarn install 9 | 10 | COPY . . 11 | RUN yarn run build 12 | 13 | # production stage 14 | FROM nginx:stable-alpine AS production-stage 15 | 16 | COPY --from=build-stage /app/dist /usr/share/nginx/html 17 | COPY .docker/nginx/init-scripts/ /docker-entrypoint.d/ 18 | 19 | COPY .docker/nginx/conf/nginx-substitute.sh . 20 | COPY .docker/nginx/conf/default.conf.template . 21 | 22 | EXPOSE ${PORT} 23 | ENTRYPOINT [ "/bin/sh", "nginx-substitute.sh"] -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import mantine from "eslint-config-mantine"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | export default tseslint.config(...mantine, { 5 | ignores: ["**/*.{mjs,cjs,js,d.ts,d.mts}", "./.storybook/main.ts"], 6 | }); 7 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Budget Board 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-preset-mantine": {}, 4 | "postcss-simple-vars": { 5 | variables: { 6 | "mantine-breakpoint-xs": "36em", 7 | "mantine-breakpoint-sm": "48em", 8 | "mantine-breakpoint-md": "62em", 9 | "mantine-breakpoint-lg": "75em", 10 | "mantine-breakpoint-xl": "88em", 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /client/public/budgetBoardIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 13 | 21 | 25 | 29 | 30 | 31 | 37 | 38 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import "@mantine/core/styles.css"; 3 | import "@mantine/notifications/styles.css"; 4 | import "@mantine/notifications/styles.layer.css"; 5 | import "@mantine/dates/styles.css"; 6 | import "@mantine/charts/styles.css"; 7 | 8 | import { BrowserRouter, Route, Routes } from "react-router"; 9 | import { createTheme, MantineProvider } from "@mantine/core"; 10 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 11 | import { Notifications } from "@mantine/notifications"; 12 | 13 | import Welcome from "~/app/Unauthorized/Welcome"; 14 | import AuthProvider from "~/components/AuthProvider/AuthProvider"; 15 | import AuthorizedRoute from "~/components/AuthProvider/AuthorizedRoute"; 16 | import UnauthorizedRoute from "~/components/AuthProvider/UnauthorizedRoute"; 17 | import Authorized from "~/app/Authorized/Authorized"; 18 | 19 | // Your theme configuration is merged with default theme 20 | const theme = createTheme({ 21 | defaultRadius: "xs", 22 | primaryColor: "indigo", 23 | }); 24 | 25 | const queryClient = new QueryClient({ 26 | defaultOptions: { 27 | queries: { 28 | staleTime: 30000, 29 | }, 30 | }, 31 | }); 32 | 33 | function App() { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | } 48 | /> 49 | 53 | 54 | 55 | } 56 | /> 57 | 58 | 59 | {/* */} 60 | 61 | 62 | 63 | ); 64 | } 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Authorized.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color: light-dark( 3 | var(--mantine-primary-color-3), 4 | var(--mantine-color-dark-9) 5 | ); 6 | } 7 | 8 | .header { 9 | background-color: light-dark( 10 | var(--mantine-color-gray-2), 11 | var(--mantine-color-dark-8) 12 | ); 13 | } 14 | 15 | .main { 16 | display: flex; 17 | flex-direction: column; 18 | background-color: light-dark( 19 | var(--mantine-color-gray-1), 20 | var(--mantine-color-dark-8) 21 | ); 22 | } 23 | 24 | .burger { 25 | &:hover { 26 | background-color: light-dark( 27 | var(--mantine-color-gray-0), 28 | var(--mantine-color-dark-5) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import classes from "./Authorized.module.css"; 2 | 3 | import { 4 | AppShell, 5 | AppShellHeader, 6 | AppShellMain, 7 | AppShellNavbar, 8 | } from "@mantine/core"; 9 | import Navbar from "./Navbar/Navbar"; 10 | import React from "react"; 11 | import PageContent, { Pages } from "./PageContent/PageContent"; 12 | import Header from "./Header/Header"; 13 | import { useDisclosure } from "@mantine/hooks"; 14 | 15 | const Authorized = (): React.ReactNode => { 16 | const [currentPage, setCurrentPage] = React.useState(Pages.Dashboard); 17 | const [isNavbarOpen, { toggle }] = useDisclosure(); 18 | 19 | const onPageSelect = (page: Pages): void => { 20 | setCurrentPage(page); 21 | toggle(); 22 | }; 23 | 24 | return ( 25 | 38 | 39 |
40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default Authorized; 57 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 6px 6px; 3 | height: 100%; 4 | justify-content: space-between; 5 | align-items: center; 6 | flex-wrap: nowrap; 7 | } 8 | 9 | .logo { 10 | flex: 0 1 auto; 11 | } 12 | 13 | .syncButton { 14 | flex: 1 0 auto; 15 | justify-content: flex-end; 16 | } 17 | 18 | .burger { 19 | &:hover { 20 | background-color: light-dark( 21 | var(--mantine-color-gray-0), 22 | var(--mantine-color-dark-5) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import BudgetBoardLogo from "~/assets/budget-board-logo"; 2 | import classes from "./Header.module.css"; 3 | 4 | import { Burger, Flex, Group, useComputedColorScheme } from "@mantine/core"; 5 | import SyncButton from "./SyncButton/SyncButton"; 6 | 7 | interface HeaderProps { 8 | isNavbarOpen: boolean; 9 | toggleNavbar: () => void; 10 | } 11 | 12 | const Header = (props: HeaderProps): React.ReactNode => { 13 | const computedColorScheme = useComputedColorScheme(); 14 | return ( 15 | 16 | 17 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Header; 37 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Header/SyncButton/SyncButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mantine/core"; 2 | import { notifications } from "@mantine/notifications"; 3 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 4 | import { AxiosResponse, AxiosError } from "axios"; 5 | import React from "react"; 6 | import { AuthContext } from "~/components/AuthProvider/AuthProvider"; 7 | import { translateAxiosError } from "~/helpers/requests"; 8 | 9 | const SyncButton = (): React.ReactNode => { 10 | const { request } = React.useContext(AuthContext); 11 | 12 | const queryClient = useQueryClient(); 13 | const doSyncMutation = useMutation({ 14 | mutationFn: async () => 15 | await request({ url: "/api/simplefin/sync", method: "GET" }), 16 | onSuccess: async (data: AxiosResponse) => { 17 | await queryClient.invalidateQueries({ queryKey: ["transactions"] }); 18 | await queryClient.invalidateQueries({ queryKey: ["institutions"] }); 19 | await queryClient.invalidateQueries({ queryKey: ["accounts"] }); 20 | await queryClient.invalidateQueries({ queryKey: ["goals"] }); 21 | if ((data.data?.length ?? 0) > 0) { 22 | { 23 | data.data.map((error: string) => 24 | notifications.show({ color: "red", message: error }) 25 | ); 26 | } 27 | } 28 | }, 29 | onError: (error: AxiosError) => { 30 | notifications.show({ color: "red", message: translateAxiosError(error) }); 31 | }, 32 | }); 33 | 34 | return ( 35 | 41 | ); 42 | }; 43 | 44 | export default SyncButton; 45 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Navbar/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .burger { 2 | align-self: flex-end; 3 | margin: var(--mantine-spacing-sm) var(--mantine-spacing-sm); 4 | 5 | &:hover { 6 | background-color: light-dark( 7 | var(--mantine-color-gray-0), 8 | var(--mantine-color-dark-5) 9 | ); 10 | } 11 | } 12 | 13 | .link { 14 | padding: var(--mantine-spacing-sm) var(--mantine-spacing-sm); 15 | height: 50px; 16 | border-radius: var(--mantine-radius-md); 17 | color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0)); 18 | 19 | &:hover { 20 | background-color: light-dark( 21 | var(--mantine-color-gray-0), 22 | var(--mantine-color-dark-5) 23 | ); 24 | } 25 | 26 | &[data-active] { 27 | &, 28 | &:hover { 29 | background-color: light-dark( 30 | var(--mantine-primary-color-4), 31 | var(--mantine-color-dark-6) 32 | ); 33 | color: light-dark( 34 | var(--mantine-color-gray-3), 35 | var(--mantine-color-dark-0) 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/src/app/Authorized/Navbar/NavbarLink.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, UnstyledButton, Text, Group } from "@mantine/core"; 2 | 3 | import classes from "./Navbar.module.css"; 4 | 5 | interface NavbarLinkProps { 6 | icon: React.ReactNode; 7 | label: string; 8 | active?: boolean; 9 | onClick?: () => void; 10 | } 11 | 12 | const NavbarLink = (props: NavbarLinkProps): React.ReactNode => { 13 | return ( 14 | 19 | 24 | 25 | {props.icon} 26 | {props.label} 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default NavbarLink; 34 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/Budgets.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | max-width: 1400px; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetGroupHeader/BudgetsGroupHeader.module.css: -------------------------------------------------------------------------------- 1 | .categoryHeader { 2 | font-size: 1rem; 3 | font-weight: 600; 4 | 5 | @container (max-width: 300px) { 6 | font-size: 0.8rem; 7 | } 8 | } 9 | 10 | .dataHeader { 11 | font-size: 1rem; 12 | font-weight: 500; 13 | 14 | @container (max-width: 300px) { 15 | font-size: 0.8rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetGroupHeader/BudgetsGroupHeader.tsx: -------------------------------------------------------------------------------- 1 | import cardClasses from "../BudgetsGroup/BudgetParentCard/BudgetParentCard.module.css"; 2 | import groupClasses from "./BudgetsGroupHeader.module.css"; 3 | 4 | import { Group, Text } from "@mantine/core"; 5 | import React from "react"; 6 | 7 | interface BudgetGroupHeaderProps { 8 | groupName: string; 9 | } 10 | 11 | const BudgetsGroupHeader = (props: BudgetGroupHeaderProps): React.ReactNode => { 12 | return ( 13 | 14 | 15 | {props.groupName} 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default BudgetsGroupHeader; 22 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetTotalCard/BudgetTotalCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 0.5rem; 3 | display: flex; 4 | flex-direction: column; 5 | gap: 0.5rem; 6 | } 7 | 8 | .group { 9 | display: flex; 10 | gap: 0.5rem; 11 | padding: 0.5rem; 12 | background-color: light-dark( 13 | var(--mantine-color-gray-4), 14 | var(--mantine-color-dark-7) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetTotalCard/BudgetTotalItem/BudgetTotalItem.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | gap: 0; 3 | } 4 | 5 | .dataContainer { 6 | gap: 0; 7 | justify-content: space-between; 8 | } 9 | 10 | .text { 11 | font-size: 1rem; 12 | font-weight: 600; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetsGroup/BudgetParentCard/BudgetChildCard/BudgetChildCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | outline: 1px solid var(--mantine-color-default-border); 3 | background-color: light-dark( 4 | var(--mantine-color-gray-1), 5 | var(--mantine-color-dark-8) 6 | ); 7 | } 8 | 9 | .budgetCard { 10 | &:hover { 11 | outline: 1px solid var(--mantine-primary-color-filled); 12 | } 13 | } 14 | 15 | .title { 16 | font-size: 1rem; 17 | 18 | @container (max-width: 380px) { 19 | font-size: 1rem; 20 | } 21 | } 22 | 23 | .text { 24 | font-size: 1rem; 25 | 26 | @container (max-width: 380px) { 27 | font-size: 0.9rem; 28 | } 29 | } 30 | 31 | .textSmall { 32 | font-size: 0.9rem; 33 | 34 | @container (max-width: 300px) { 35 | font-size: 0.8rem; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetsGroup/BudgetParentCard/BudgetParentCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | outline: 1px solid var(--mantine-color-default-border); 3 | background-color: light-dark( 4 | var(--mantine-color-gray-1), 5 | var(--mantine-color-dark-8) 6 | ); 7 | } 8 | 9 | .budgetCard { 10 | &:hover { 11 | outline: 1px solid var(--mantine-primary-color-filled); 12 | } 13 | } 14 | 15 | .title { 16 | font-size: 1.2rem; 17 | 18 | @container (max-width: 380px) { 19 | font-size: 1rem; 20 | } 21 | } 22 | 23 | .text { 24 | font-size: 1.1rem; 25 | 26 | @container (max-width: 380px) { 27 | font-size: 1rem; 28 | } 29 | } 30 | 31 | .textSmall { 32 | font-size: 1rem; 33 | 34 | @container (max-width: 300px) { 35 | font-size: 0.8rem; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetsGroup/BudgetParentCard/UnbudgetChildCard/UnbudgetChildCard.module.css: -------------------------------------------------------------------------------- 1 | .text { 2 | font-size: 1rem; 3 | 4 | @container (max-width: 380px) { 5 | font-size: 0.9rem; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetsGroup/BudgetsGroup.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | gap: 0.5rem; 3 | align-items: center; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/BudgetsGroup/BudgetsGroup.tsx: -------------------------------------------------------------------------------- 1 | import classes from "./BudgetsGroup.module.css"; 2 | 3 | import { Stack, Text } from "@mantine/core"; 4 | import { IBudget } from "~/models/budget"; 5 | import React from "react"; 6 | import { ICategory, ICategoryNode } from "~/models/category"; 7 | import { 8 | buildCategoryToBudgetsMap, 9 | buildCategoryToLimitsMap, 10 | } from "~/helpers/budgets"; 11 | import BudgetParentCard from "./BudgetParentCard/BudgetParentCard"; 12 | 13 | interface BudgetsGroupProps { 14 | budgets: IBudget[]; 15 | categoryToTransactionsTotalMap: Map; 16 | categoryTree: ICategoryNode[]; 17 | categories: ICategory[]; 18 | selectedDate?: Date; 19 | } 20 | 21 | const BudgetsGroup = (props: BudgetsGroupProps): React.ReactNode => { 22 | const categoryToBudgetsMap = buildCategoryToBudgetsMap(props.budgets); 23 | const categoryToLimitsMap = buildCategoryToLimitsMap( 24 | props.budgets, 25 | props.categoryTree 26 | ); 27 | 28 | return ( 29 | 30 | {props.budgets.length > 0 ? ( 31 | props.categoryTree.map((category) => { 32 | if ( 33 | categoryToBudgetsMap.has(category.value.toLocaleLowerCase()) || 34 | category.subCategories.some((subCategory) => 35 | categoryToBudgetsMap.has(subCategory.value.toLocaleLowerCase()) 36 | ) 37 | ) { 38 | return ( 39 | 49 | ); 50 | } 51 | return null; 52 | }) 53 | ) : ( 54 | No budgets. 55 | )} 56 | 57 | ); 58 | }; 59 | 60 | export default BudgetsGroup; 61 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/UnbudgetedGroup/UnbudgetedCard/UnbudgetedCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: light-dark( 3 | var(--mantine-color-gray-2), 4 | var(--mantine-color-dark-4) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/UnbudgetedGroup/UnbudgetedCard/UnbudgetedChildCard/UnbudgetedChildCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: light-dark( 3 | var(--mantine-color-gray-2), 4 | var(--mantine-color-dark-4) 5 | ); 6 | } 7 | 8 | .text { 9 | font-size: 0.9rem; 10 | 11 | @container (max-width: 380px) { 12 | font-size: 0.8rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsContent/UnbudgetedGroup/UnbudgetedGroup.module.css: -------------------------------------------------------------------------------- 1 | .accordion { 2 | outline: 1px solid 3 | light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8)); 4 | background-color: light-dark( 5 | var(--mantine-color-white), 6 | var(--mantine-color-dark-6) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Budgets/BudgetsToolbar/AddBudget/AddBudget.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 0.5rem; 3 | } 4 | 5 | .formContainer { 6 | display: flex; 7 | flex-direction: row; 8 | gap: 0.5rem; 9 | } 10 | 11 | .submitContainer { 12 | align-self: stretch; 13 | } 14 | .submitButton { 15 | height: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsCard.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | width: 100%; 3 | height: auto; 4 | display: flex; 5 | flex-direction: column; 6 | gap: 12px; 7 | background-color: light-dark( 8 | var(--mantine-color-gray-0), 9 | var(--mantine-color-dark-6) 10 | ); 11 | } 12 | 13 | .settingsIcon { 14 | color: var(--mantine-color-text); 15 | 16 | &:hover { 17 | color: var(--mantine-color-primary); 18 | } 19 | } 20 | 21 | .headerContainer { 22 | justify-content: space-between; 23 | align-items: center; 24 | } 25 | 26 | .accountsContainer { 27 | gap: 0.5rem; 28 | align-items: center; 29 | width: 100%; 30 | } 31 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettings.module.css: -------------------------------------------------------------------------------- 1 | .settingsIcon { 2 | color: var(--mantine-color-text); 3 | 4 | &:hover { 5 | color: var(--mantine-color-primary); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettings.tsx: -------------------------------------------------------------------------------- 1 | import classes from "./AccountsSettings.module.css"; 2 | 3 | import { ActionIcon, useModalsStack } from "@mantine/core"; 4 | import { SettingsIcon } from "lucide-react"; 5 | import React from "react"; 6 | import AccountsSettingsModal from "./AccountsSettingsModal/AccountsSettingsModal"; 7 | import { IInstitution } from "~/models/institution"; 8 | import { IAccount } from "~/models/account"; 9 | import CreateAccountModal from "./CreateAccountModal/CreateAccountModal"; 10 | 11 | interface AccountsSettingsProps { 12 | sortedFilteredInstitutions: IInstitution[]; 13 | accounts: IAccount[]; 14 | } 15 | 16 | const AccountsSettings = (props: AccountsSettingsProps): React.ReactNode => { 17 | const stack = useModalsStack(["settings", "createAccount"]); 18 | 19 | return ( 20 |
21 | stack.open("createAccount")} 25 | {...stack.register("settings")} 26 | /> 27 | 28 | stack.open("settings")} 32 | > 33 | 34 | 35 |
36 | ); 37 | }; 38 | export default AccountsSettings; 39 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettingsModal/AccountsSettingsModal.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | flex-direction: row; 3 | flex: 1 1 auto; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettingsModal/DeletedAccounts/DeletedAccountCard/DeletedAccountCard.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 12px; 7 | width: 100%; 8 | background-color: light-dark( 9 | var(--mantine-color-gray-2), 10 | var(--mantine-color-dark-7) 11 | ); 12 | } 13 | 14 | .column { 15 | flex: 1; 16 | } 17 | 18 | .button { 19 | flex: 0; 20 | justify-content: flex-end; 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettingsModal/DeletedAccounts/DeletedAccountCard/DeletedAccountCard.tsx: -------------------------------------------------------------------------------- 1 | import { AuthContext } from "~/components/AuthProvider/AuthProvider"; 2 | import classes from "./DeletedAccountCard.module.css"; 3 | 4 | import { getDaysSinceDate } from "~/helpers/datetime"; 5 | import { Button, Card, Group, Text } from "@mantine/core"; 6 | import { IAccount } from "~/models/account"; 7 | import { Undo2Icon } from "lucide-react"; 8 | import React from "react"; 9 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 10 | import { notifications } from "@mantine/notifications"; 11 | import { translateAxiosError } from "~/helpers/requests"; 12 | import { AxiosError } from "axios"; 13 | 14 | interface DeletedAccountCardProps { 15 | deletedAccount: IAccount; 16 | } 17 | 18 | const DeletedAccountCard = ( 19 | props: DeletedAccountCardProps 20 | ): React.ReactNode => { 21 | const { request } = React.useContext(AuthContext); 22 | 23 | const queryClient = useQueryClient(); 24 | const doRestoreAccount = useMutation({ 25 | mutationFn: async (id: string) => 26 | await request({ 27 | url: "/api/account/restore", 28 | method: "POST", 29 | params: { guid: id }, 30 | }), 31 | onSuccess: async () => { 32 | // Refetch the accounts and institutions queries immediatly after the account is restored 33 | await queryClient.refetchQueries({ queryKey: ["institutions"] }); 34 | await queryClient.refetchQueries({ queryKey: ["accounts"] }); 35 | }, 36 | onError: (error: AxiosError) => { 37 | notifications.show({ color: "red", message: translateAxiosError(error) }); 38 | }, 39 | }); 40 | 41 | return ( 42 | 43 | {props.deletedAccount.name} 44 | 45 | {`${getDaysSinceDate( 46 | props.deletedAccount.deleted! 47 | )} days since deleted`} 48 | 49 | 50 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default DeletedAccountCard; 62 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettingsModal/DeletedAccounts/DeletedAccounts.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, Stack } from "@mantine/core"; 2 | import { IAccount } from "~/models/account"; 3 | import React from "react"; 4 | import DeletedAccountCard from "./DeletedAccountCard/DeletedAccountCard"; 5 | 6 | interface DeletedAccountsProps { 7 | deletedAccounts: IAccount[]; 8 | } 9 | 10 | const DeletedAccounts = (props: DeletedAccountsProps): React.ReactNode => { 11 | const sortedDeletedAccounts = props.deletedAccounts.sort( 12 | (a, b) => 13 | new Date(b.deleted ?? 0).getTime() - new Date(a.deleted ?? 0).getTime() 14 | ); 15 | return ( 16 | 17 | 18 | Deleted Accounts 19 | 20 | 21 | {sortedDeletedAccounts.map((deletedAccount) => ( 22 | 26 | ))} 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default DeletedAccounts; 35 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/AccountsSettings/AccountsSettingsModal/InstitutionSettingsCard/InstitutionSettingsCard.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: light-dark( 3 | var(--mantine-color-gray-2), 4 | var(--mantine-color-dark-5) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/InstitutionItems/AccountItem/AccountItem.tsx: -------------------------------------------------------------------------------- 1 | import { convertNumberToCurrency } from "~/helpers/currency"; 2 | import { Group, Stack, Text } from "@mantine/core"; 3 | import { AccountSource, IAccount } from "~/models/account"; 4 | import React from "react"; 5 | import { AuthContext } from "~/components/AuthProvider/AuthProvider"; 6 | import { useQuery } from "@tanstack/react-query"; 7 | import { IUserSettings } from "~/models/userSettings"; 8 | import { AxiosResponse } from "axios"; 9 | 10 | interface AccountItemProps { 11 | account: IAccount; 12 | } 13 | 14 | const AccountItem = (props: AccountItemProps): React.ReactNode => { 15 | const { request } = React.useContext(AuthContext); 16 | 17 | const userSettingsQuery = useQuery({ 18 | queryKey: ["userSettings"], 19 | queryFn: async (): Promise => { 20 | const res: AxiosResponse = await request({ 21 | url: "/api/userSettings", 22 | method: "GET", 23 | }); 24 | 25 | if (res.status === 200) { 26 | return res.data as IUserSettings; 27 | } 28 | 29 | return undefined; 30 | }, 31 | }); 32 | 33 | return ( 34 | 35 | 36 | {props.account.name} 37 | {userSettingsQuery.isPending ? null : ( 38 | 47 | {convertNumberToCurrency( 48 | props.account.currentBalance, 49 | true, 50 | userSettingsQuery.data?.currency ?? "USD" 51 | )} 52 | 53 | )} 54 | 55 | {props.account.source !== AccountSource.Manual && ( 56 | 60 | {"Last updated: "} 61 | {props.account.balanceDate 62 | ? new Date(props.account.balanceDate).toLocaleString() 63 | : "Never!"} 64 | 65 | )} 66 | 67 | ); 68 | }; 69 | 70 | export default AccountItem; 71 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/InstitutionItems/InstitutionItem.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | width: 100%; 3 | background-color: light-dark( 4 | var(--mantine-color-gray-2), 5 | var(--mantine-color-dark-7) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/AccountsCard/InstitutionItems/InstitutionItem.tsx: -------------------------------------------------------------------------------- 1 | import classes from "./InstitutionItem.module.css"; 2 | 3 | import { filterVisibleAccounts } from "~/helpers/accounts"; 4 | import { Card, Divider, Stack, Title } from "@mantine/core"; 5 | import { IAccount } from "~/models/account"; 6 | import { IInstitution } from "~/models/institution"; 7 | import { DndContext } from "@dnd-kit/core"; 8 | import { SortableContext } from "@dnd-kit/sortable"; 9 | 10 | import React from "react"; 11 | import AccountItem from "./AccountItem/AccountItem"; 12 | 13 | interface InstitutionItemProps { 14 | institution: IInstitution; 15 | } 16 | 17 | const InstitutionItem = (props: InstitutionItemProps): React.ReactNode => { 18 | const sortedFilteredAccounts = filterVisibleAccounts( 19 | props.institution.accounts 20 | ).sort((a, b) => a.index - b.index); 21 | 22 | return ( 23 | 24 | 25 | {props.institution.name} 26 | 27 | 28 | 29 | 30 | 31 | {sortedFilteredAccounts.map((account: IAccount) => ( 32 | 33 | ))} 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default InstitutionItem; 42 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .dashboardContainer { 2 | width: 100%; 3 | max-width: 1400px; 4 | height: 100%; 5 | flex: 1; 6 | } 7 | 8 | .mainContent { 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: center; 12 | align-items: flex-start; 13 | flex-wrap: nowrap; 14 | gap: 12px; 15 | flex: 1; 16 | 17 | @media (max-width: $mantine-breakpoint-sm) { 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | } 22 | 23 | .accountColumn { 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: flex-start; 27 | align-items: center; 28 | width: 40%; 29 | max-width: 400px; 30 | height: 100%; 31 | 32 | @media (max-width: $mantine-breakpoint-sm) { 33 | width: 100%; 34 | max-width: 100%; 35 | } 36 | } 37 | 38 | .feedColumn { 39 | flex-grow: 1; 40 | display: flex; 41 | flex-direction: column; 42 | justify-content: flex-start; 43 | align-items: center; 44 | height: 100%; 45 | width: 60%; 46 | gap: 12px; 47 | 48 | @media (max-width: $mantine-breakpoint-sm) { 49 | width: 100%; 50 | max-width: 100%; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/app/Authorized/PageContent/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import classes from "./Dashboard.module.css"; 2 | 3 | import { Flex, Stack } from "@mantine/core"; 4 | import React from "react"; 5 | import AccountsCard from "./AccountsCard/AccountsCard"; 6 | import NetWorthCard from "./NetWorthCard/NetWorthCard"; 7 | import Footer from "./Footer/Footer"; 8 | import SpendingTrendsCard from "./SpendingTrendsCard/SpendingTrendsCard"; 9 | import UncategorizedTransactionsCard from "./UncategorizedTransactionsCard/UncategorizedTransactionsCard"; 10 | 11 | const Dashboard = (): React.ReactNode => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |