diff --git a/.dockerignore b/.dockerignore index c9fec49..d6cc944 100644 --- a/.dockerignore +++ b/.dockerignore @@ -30,6 +30,7 @@ README.md # files Dockerfile* +**/appsettings.Development.json* **/*.trx **/*.md **/*.ps1 diff --git a/.github/workflows/docker-image-master.yml b/.github/workflows/docker-image-master.yml index 692c5d6..e1bbb18 100644 --- a/.github/workflows/docker-image-master.yml +++ b/.github/workflows/docker-image-master.yml @@ -5,7 +5,7 @@ on: branches: [ master ] env: - DOTNET_VERSION: '7.0.103' # The .NET SDK version to use + DOTNET_VERSION: '8.0.100' # The .NET SDK version to use jobs: test: diff --git a/.github/workflows/docker-image-pre-release.yml b/.github/workflows/docker-image-pre-release.yml index b6644d8..2e982fc 100644 --- a/.github/workflows/docker-image-pre-release.yml +++ b/.github/workflows/docker-image-pre-release.yml @@ -5,7 +5,7 @@ on: branches: [ pre-release ] env: - DOTNET_VERSION: '7.0.103' # The .NET SDK version to use + DOTNET_VERSION: '8.0.100' # The .NET SDK version to use jobs: test: @@ -28,9 +28,9 @@ jobs: - name: Run Core Test Cases run: dotnet test OpenBudgeteer.Core.Test - deploy-docker: + deploy-docker-app: runs-on: ubuntu-latest - name: Build and Push Docker Image + name: Build and Push Docker Image (App) needs: test if: success() steps: @@ -55,5 +55,33 @@ jobs: context: . push: true tags: axelander/openbudgeteer:pre-release -# file: OpenBudgeteer.Blazor/Dockerfile - platforms: linux/amd64 + platforms: linux/arm64,linux/amd64 + deploy-docker-api: + runs-on: ubuntu-latest + name: Build and Push Docker Image (API) + needs: test + if: success() + steps: + - name: Check out repo + uses: actions/checkout@v3 + + - name: Docker Login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: axelander/openbudgeteer-api:pre-release + file: API.Dockerfile + platforms: linux/arm64,linux/amd64 \ No newline at end of file diff --git a/.github/workflows/docker-image-version-tag.yml b/.github/workflows/docker-image-version-tag.yml new file mode 100644 index 0000000..b69b700 --- /dev/null +++ b/.github/workflows/docker-image-version-tag.yml @@ -0,0 +1,59 @@ +name: Docker Image (Version Tag) + +on: + push: + tags: + - "*" + +env: + DOTNET_VERSION: '8.0.100' # The .NET SDK version to use + +jobs: + test: + runs-on: ubuntu-latest + name: Run Test Cases + steps: + - name: Check out repo + uses: actions/checkout@v3 + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install Blazor dependencies + run: dotnet restore OpenBudgeteer.Blazor + + - name: Build Blazor + run: dotnet build OpenBudgeteer.Blazor --configuration Release --no-restore + + - name: Run Core Test Cases + run: dotnet test OpenBudgeteer.Core.Test + deploy-docker: + runs-on: ubuntu-latest + name: Build and Push Docker Image + needs: test + if: success() + steps: + - name: Check out repo + uses: actions/checkout@v3 + + - name: Docker Login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: axelander/openbudgeteer:${{ github.ref_name }} + platforms: linux/arm64,linux/amd64 diff --git a/.gitignore b/.gitignore index 8b3aafa..cfd7bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ *.db-shm *.db-wal *.DS_Store +OpenBudgeteer.Blazor/appsettings.Development.json +OpenBudgeteer.API/appsettings.Development.json # User-specific files *.rsuser diff --git a/API.Dockerfile b/API.Dockerfile new file mode 100644 index 0000000..ae6eae5 --- /dev/null +++ b/API.Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base +WORKDIR /api +ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 +ENV DOTNET_RUNNING_IN_CONTAINER=true +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +RUN apk add --no-cache icu-libs icu-data-full + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG TARGETARCH +ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 +WORKDIR /src +COPY . . +RUN dotnet restore -a $TARGETARCH +WORKDIR "/src/OpenBudgeteer.API" +RUN dotnet publish "OpenBudgeteer.API.csproj" --no-self-contained -c Release -a $TARGETARCH -o /api/publish + +FROM base AS final +WORKDIR /api +COPY --from=build /api/publish . +ENTRYPOINT ["dotnet", "OpenBudgeteer.API.dll"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 000b98a..cfe82f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,45 @@ -### 1.7.1 (2023-10-16) +## 1.8 (2024-03-16) + +### :warning: Breaking Changes + +* Docker Image now listens on port 8080 (new .Net 8 default) + +### :gear: Features & Enhancements + +* First version of Web API in a separate Docker Image [#127](https://github.com/TheAxelander/OpenBudgeteer/issues/127) +* Buttons that appeared in the past only via hovering are now always displayed (was not working well, maybe additional rework in future) [#155](https://github.com/TheAxelander/OpenBudgeteer/issues/155) +* More responsive navigation bar [#175](https://github.com/TheAxelander/OpenBudgeteer/issues/175) +* New Data Consistency Check: Negative Transaction assigned to Income [#177](https://github.com/TheAxelander/OpenBudgeteer/issues/177) +* Create Transaction keeps last input date [#187](https://github.com/TheAxelander/OpenBudgeteer/issues/187) +* Consistent currency and number format for displayed Amounts [#188](https://github.com/TheAxelander/OpenBudgeteer/issues/188) +* An empty database can be loaded now with some demo data using `APPSETTINGS_DEMO_DATA: true` [#192](https://github.com/TheAxelander/OpenBudgeteer/issues/192) +* Enable editing of imported file [#196](https://github.com/TheAxelander/OpenBudgeteer/issues/196) +* Text Color option for Bucket [#207](https://github.com/TheAxelander/OpenBudgeteer/issues/207) +* Confirmation Dialog for deleting a Bucket Group [#208](https://github.com/TheAxelander/OpenBudgeteer/issues/208) +* Loading screen while opening/uploading a file [#209](https://github.com/TheAxelander/OpenBudgeteer/issues/209) +* Rework UI to make it more responsive for various small screen sizes [#211](https://github.com/TheAxelander/OpenBudgeteer/issues/211) +* Redesign of Navigation Menu and Footer +* Redesign Banner message on unwanted exceptions + +### :beetle: Bug Fixes + +* UI alignment for large numbers [#189](https://github.com/TheAxelander/OpenBudgeteer/issues/189) +* Mapping Rules using the Account Name instead of Account Id [#199](https://github.com/TheAxelander/OpenBudgeteer/pull/199) Thanks [Lucaber](https://github.com/Lucaber) +* Adding a Mapping Rule to an existing Rule Set no longer fails [#200](https://github.com/TheAxelander/OpenBudgeteer/pull/200) Thanks [Lucaber](https://github.com/Lucaber) +* Exception after canceling creation of a new Recurring Transactions [#210](https://github.com/TheAxelander/OpenBudgeteer/issues/210) +* Sqlite database migration issue (Guid generation) since `1.7` [#221](https://github.com/TheAxelander/OpenBudgeteer/issues/221) +* Fix `Expand All` button typo on Bucket Page [#224](https://github.com/TheAxelander/OpenBudgeteer/issues/224) + +### :hammer: Maintenance + +* Builds for ARM64 are available again [#131](https://github.com/TheAxelander/OpenBudgeteer/issues/131) +* Migrated to .Net 8 [#198](https://github.com/TheAxelander/OpenBudgeteer/issues/198) + +## 1.7.1 (2023-10-16) * [Fixed] Potentially fixed crashes on Rules page due to an unnecessary parallel initialization [#165](https://github.com/TheAxelander/OpenBudgeteer/issues/165) -### 1.7 (2023-10-03) +## 1.7 (2023-10-03) * [Add] PostgreSQL support [#81](https://github.com/TheAxelander/OpenBudgeteer/issues/81) Thanks [csillaggyujto](https://github.com/csillaggyujto) * [Add] Confirmation Dialog before setting a Bucket inactive [#119](https://github.com/TheAxelander/OpenBudgeteer/issues/119) @@ -30,22 +67,22 @@ * [Fixed] Fix navbar in portrait mode [#190](https://github.com/TheAxelander/OpenBudgeteer/pull/190) Thanks [Lucaber](https://github.com/Lucaber) * [Known issue] Update 1.7 doesn't compile currently for ARM64 -### 1.6.3 (2023-01-27) +## 1.6.3 (2023-01-27) * [Add] Several Confirmation Dialogs for Import Profile handling [#124](https://github.com/TheAxelander/OpenBudgeteer/issues/124) * [Fixed] Proper reset of values after deleting an Import Profile [#125](https://github.com/TheAxelander/OpenBudgeteer/issues/125) * [Fixed] Overall improved and fixed error handling on Import Page -### 1.6.2 (2023-01-11) +## 1.6.2 (2023-01-11) * [Fixed] Due to implemented fix for [#114](https://github.com/TheAxelander/OpenBudgeteer/issues/114) Column mapping on Import Page was not working properly [#121](https://github.com/TheAxelander/OpenBudgeteer/issues/121) [#122](https://github.com/TheAxelander/OpenBudgeteer/issues/122) -### 1.6.1 (2022-12-31) +## 1.6.1 (2022-12-31) * [Changed] Slight visual changes for Mapping Rule modification * [Fixed] Rendering of Drop-down selection element for Firefox browser [#114](https://github.com/TheAxelander/OpenBudgeteer/issues/114) -### 1.6 (2022-12-03) +## 1.6 (2022-12-03) * [Add] Enhanced Bucket assignment for Bank Transaction (display remaining amount, manual triggered split) Thanks [ambroser1971](https://github.com/ambroser1971) * [Add] Recurring Transactions [#74](https://github.com/TheAxelander/OpenBudgeteer/issues/74) @@ -68,16 +105,16 @@ * [Fixed] Correct number of months shown on Report Page (e.g. should show past 24 months but displays 25 months) * [Fixed] Buckets that have been marked as inactive no longer display Want [#108](https://github.com/TheAxelander/OpenBudgeteer/issues/108) -### 1.5.2 (2022-03-26) +## 1.5.2 (2022-03-26) * [Fixed] Sqlite database lock while saving multiple Bank Transaction [#90](https://github.com/TheAxelander/OpenBudgeteer/issues/90) -### 1.5.1 (2022-02-26) +## 1.5.1 (2022-02-26) * [Fixed] Amount conversion with currency characters. [#82](https://github.com/TheAxelander/OpenBudgeteer/issues/82) [#83](https://github.com/TheAxelander/OpenBudgeteer/pull/83) Thanks [Hazy87](https://github.com/Hazy87) * [Fixed] Amount conversion with 0 values. [#72](https://github.com/TheAxelander/OpenBudgeteer/issues/72) -### 1.5 (2022-02-19) +## 1.5 (2022-02-19) * [Add] Option to set Localization [#52](https://github.com/TheAxelander/OpenBudgeteer/issues/52) * [Add] Enable mapping of seperated columns for Debit and Credit Amount on Import Page [#53](https://github.com/TheAxelander/OpenBudgeteer/issues/53) @@ -86,13 +123,13 @@ * [Fixed] Crash on Rules Page in case a Bucket has been deleted with an existing RuleSet [#65](https://github.com/TheAxelander/OpenBudgeteer/issues/65) * [Fixed] Include Transactions which are in modification in all filters to prevent immediate disappearance [#67](https://github.com/TheAxelander/OpenBudgeteer/issues/67) -### 1.4.1 (2021-11-28) +## 1.4.1 (2021-11-28) * [Changed] Handling of Bucket Group creation (fixes also crashes during creation cancellation [#56](https://github.com/TheAxelander/OpenBudgeteer/issues/56)) * [Fixed] Unable to add multiple Buckets during Bank Transaction creation [#55](https://github.com/TheAxelander/OpenBudgeteer/issues/55) * [Fixed] Crash on Report Page using sqlite [#57](https://github.com/TheAxelander/OpenBudgeteer/issues/57) -### 1.4 (2021-11-14) +## 1.4 (2021-11-14) * [Add] Info Dialog during Bucket proposal and optimized proposal performance [#21](https://github.com/TheAxelander/OpenBudgeteer/issues/21) * [Add] Filter on Transaction Page [#25](https://github.com/TheAxelander/OpenBudgeteer/issues/25) @@ -113,16 +150,16 @@ * [Fixed] Trigger of `SelectedYearMonthChanged` passing `OpenBudgeteer.Core.Test.ViewModelTest.SelectedYearMonthChanged_CheckEventHasBeenInvoked` Test * [Fixed] Wrong text in confirmation message box for deleting a Rule [#44](https://github.com/TheAxelander/OpenBudgeteer/issues/44) -### 1.3 (2020-12-15) +## 1.3 (2020-12-15) * [Add] Support for Sqlite databases [#2](https://github.com/TheAxelander/OpenBudgeteer/issues/2) * [Add] Unit Tests (not full coverage yet) -### 1.2.1 (2020-12-14) +## 1.2.1 (2020-12-14) * [Fixed] Crash on Report Page due to wrong DateTime creation -### 1.2 (2020-10-26) +## 1.2 (2020-10-26) * [Add] Enable collapse of Bucket Groups * [Changed] Overall style changes with new font and colors @@ -130,13 +167,13 @@ * [Changed] Style for Bucket Group modification * [Fixed] Unable to move newly created Bucket Groups [#16](https://github.com/TheAxelander/OpenBudgeteer/issues/16) -### 1.1.1 (2020-09-07) +## 1.1.1 (2020-09-07) * [Fixed] Wrong creation of data for new Rules if the initial selection was used [#13](https://github.com/TheAxelander/OpenBudgeteer/issues/13) * [Fixed] Missing months for Monthly Bucket Expenses Reports in case of no data [#14](https://github.com/TheAxelander/OpenBudgeteer/issues/14) * [Fixed] Crashes on Report Page due to display split of Monthly Bucket Expenses Reports [#15](https://github.com/TheAxelander/OpenBudgeteer/issues/15) -### 1.1 (2020-09-05) +## 1.1 (2020-09-05) * [Add] Added Rule set for automatic Bucket assignments [#5](https://github.com/TheAxelander/OpenBudgeteer/issues/5) * [Add] Enabled movement of Buckets to other Bucket Groups @@ -145,12 +182,12 @@ * [Changed] Opening a new file resets previous Import selection and settings * [Changed] Optimized Y-Axis Ticks for Month Bucket Expenses on Report Page -### 1.0 (2020-08-10) +## 1.0 (2020-08-10) * First major release * Repository now Open Source -### 0.12 (2020-08-04) +## 0.12 (2020-08-04) * [Add] Current version number and database name to header * [Add] Bucket Notes @@ -160,7 +197,7 @@ * [Changed] Redesign Account Page * [Changed] Redesign Balance details on Bucket Page * [Changed] Removed Page Titles -* [Changed] Changed ConnectionString setup for Docker (Splitted full ConnectionString into several pieces for User, Password etc.) +* [Changed] Changed ConnectionString setup for Docker (Split full ConnectionString into several pieces for User, Password etc.) * [Changed] Removed Blazor in Assembly name (now `OpenBudgeteer.dll`) * [Changed] HTML Title from `OpenBudgeteer.Blazor` to `OpenBudgeteer` * [Fixed] Database update on Number and Date format for Import Profile @@ -169,11 +206,11 @@ * [Fixed] Database Issue during Bucket Deletion * [Fixed] IsInactiveFrom value for newly created Buckets -### 0.11.1 (2020-07-18) +## 0.11.1 (2020-07-18) * [Fixed] Broken responsive design for Monthly Bucket Expenses -### 0.11 (2020-07-18) +## 0.11 (2020-07-18) * [Add] Page with several Reports * [Add] Popup with Transactions assigned to an Account on Account Page @@ -190,7 +227,7 @@ * [Fixed] Wrong Error Dialog on Transaction Page * [Fixed] Re-enabled handling of inactive Accounts for existing Transactions -### 0.10 (2020-07-12) +## 0.10 (2020-07-12) * [Add] Preview of final records during data import * [Add] Options for Date and Number format for data import @@ -202,7 +239,7 @@ * [Fixed] Inconsistent number output format for 0 on Bucket Page * [Fixed] Multiple Budget distribution on Buckets -### 0.9 (2020-07-07) +## 0.9 (2020-07-07) * [Add] Added selection of Delimiter and Text qualifier during data import * [Add] Button to edit and save all Transaction @@ -213,35 +250,35 @@ * [Fixed] Missing Bucket colors for Crate new Transaction * [Fixed] Only active Buckets for current YearMonth are displayed for Bucket assignment on Transaction Page -### 0.8 (2020-07-01) +## 0.8 (2020-07-01) * [Add] Colors for Buckets * [Add] Button to distribute Budget on Buckets with Want * [Changed] Input fields for numbers and dates are now properly handled * [Fixed] Want calculation for Bucket Type "Monthly expense" -### 0.7 (2020-06-30) +## 0.7 (2020-06-30) * [Fixed] Pressing Enter for InOut updates UI again * [Fixed] Creating a new Bucket properly updates UI again * [Fixed] Fixed Want calculation due to DateTime issues in Data Model -### 0.6 (2020-06-29) +## 0.6 (2020-06-29) * [Add] Added base implementation for Global Balance details * [Changed] Optimized Performance for Bucket and Transaction Page * [Changed] Allow Update of imported Transactions with pending Bucket assignments * [Fixed] Fixed and optimized Bucket assignment check for Transactions -### 0.5 (2020-06-23) +## 0.5 (2020-06-23) * [Changed] Switch to Blazor -### 0.4 (2020-05-25) +## 0.4 (2020-05-25) * [Changed] Basis for generic database handling implemented -### 0.3 (2020-05-19) +## 0.3 (2020-05-19) * [Changed] Displayed Accounts now sorted by name * [Changed] Displayed Buckets now sorted by name @@ -251,10 +288,10 @@ * [Fixed] Transaction Update for Bucket assignments * [Fixed] Missing database table creation for BucketVersion -### 0.2 (2020-05-18) +## 0.2 (2020-05-18) -* [Add] Enbale creation/update/deletion of Import Profiles +* [Add] Enable creation/update/deletion of Import Profiles -### 0.1 (2020-04-20) +## 0.1 (2020-04-20) * Initial version diff --git a/Dockerfile b/Dockerfile index e497431..1c3f1c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,18 @@ -#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base WORKDIR /app ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_RUNNING_IN_CONTAINER=true ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false -EXPOSE 80 -EXPOSE 443 RUN apk add --no-cache icu-libs icu-data-full -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG TARGETARCH ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 WORKDIR /src COPY . . -RUN dotnet restore -r linux-musl-x64 +RUN dotnet restore -a $TARGETARCH WORKDIR "/src/OpenBudgeteer.Blazor" -RUN dotnet publish "OpenBudgeteer.Blazor.csproj" -r linux-musl-x64 --no-self-contained -c Release -o /app/publish +RUN dotnet publish "OpenBudgeteer.Blazor.csproj" --no-self-contained -c Release -a $TARGETARCH -o /app/publish FROM base AS final WORKDIR /app diff --git a/LICENSE-3RD-PARTY b/LICENSE-3RD-PARTY index b6ffa28..727a6aa 100644 --- a/LICENSE-3RD-PARTY +++ b/LICENSE-3RD-PARTY @@ -1,10 +1,12 @@ ----------------------------------------------------------------------------- The MIT License (MIT) applies to: + - aspnet-api-versioning, Copyright (c) .NET Foundation and contributors - BlazorFileReader, Copyright (c) 2018 Tor - ChartJs.Blazor, Copyright (c) 2019 Marius Muntean - efcore, Copyright (c) .NET Foundation and Contributors - Pomelo.EntityFrameworkCore.MySql, Copyright (c) 2017 Pomelo Foundation + - Swashbuckle.AspNetCore, Copyright (c) 2016 Richard Morris - TinyCsvParser, Copyright (c) Philipp Wagner and Contributors - .NET runtime, Copyright (c) .NET Foundation and Contributors - Bootstrap, Copyright (c) 2011-2018 Twitter, Inc. diff --git a/OpenBudgeteer.API/ConfigureSwaggerOptions.cs b/OpenBudgeteer.API/ConfigureSwaggerOptions.cs new file mode 100644 index 0000000..afb0b05 --- /dev/null +++ b/OpenBudgeteer.API/ConfigureSwaggerOptions.cs @@ -0,0 +1,88 @@ +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Text; + +namespace OpenBudgeteer.API; + +/// +/// Configures the Swagger generation options. +/// +/// This allows API versioning to define a Swagger document per API version after the +/// service has been resolved from the service container. +public class ConfigureSwaggerOptions : IConfigureOptions +{ + private readonly IApiVersionDescriptionProvider provider; + + /// + /// Initializes a new instance of the class. + /// + /// The provider used to generate Swagger documents. + public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) => this.provider = provider; + + /// + public void Configure( SwaggerGenOptions options ) + { + // add a swagger document for each discovered API version + // note: you might choose to skip or document deprecated API versions differently + foreach ( var description in provider.ApiVersionDescriptions ) + { + options.SwaggerDoc( description.GroupName, CreateInfoForApiVersion( description ) ); + } + } + + private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description ) + { + var text = new StringBuilder( "An example application with OpenAPI, Swashbuckle, and API versioning." ); + var info = new OpenApiInfo() + { + Title = "Example API", + Version = description.ApiVersion.ToString(), + Contact = new OpenApiContact() { Name = "Bill Mei", Email = "bill.mei@somewhere.com" }, + License = new OpenApiLicense() { Name = "MIT", Url = new Uri( "https://opensource.org/licenses/MIT" ) } + }; + + if ( description.IsDeprecated ) + { + text.Append( " This API version has been deprecated." ); + } + + if ( description.SunsetPolicy is SunsetPolicy policy ) + { + if ( policy.Date is DateTimeOffset when ) + { + text.Append( " The API will be sunset on " ) + .Append( when.Date.ToShortDateString() ) + .Append( '.' ); + } + + if ( policy.HasLinks ) + { + text.AppendLine(); + + for ( var i = 0; i < policy.Links.Count; i++ ) + { + var link = policy.Links[i]; + + if ( link.Type == "text/html" ) + { + text.AppendLine(); + + if ( link.Title.HasValue ) + { + text.Append( link.Title.Value ).Append( ": " ); + } + + text.Append( link.LinkTarget.OriginalString ); + } + } + } + } + + info.Description = text.ToString(); + + return info; + } +} \ No newline at end of file diff --git a/OpenBudgeteer.API/OpenBudgeteer.API.csproj b/OpenBudgeteer.API/OpenBudgeteer.API.csproj new file mode 100644 index 0000000..16d5dcf --- /dev/null +++ b/OpenBudgeteer.API/OpenBudgeteer.API.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + Linux + + + + + appsettings.json + Always + + + + + + + + + + + + + + + + diff --git a/OpenBudgeteer.API/Program.cs b/OpenBudgeteer.API/Program.cs new file mode 100644 index 0000000..16520dd --- /dev/null +++ b/OpenBudgeteer.API/Program.cs @@ -0,0 +1,343 @@ +using System.Globalization; +using Asp.Versioning; +using Asp.Versioning.Conventions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using OpenBudgeteer.API; +using OpenBudgeteer.Core.Data; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.Data.Entities; +using OpenBudgeteer.Core.Data.Entities.Models; +using OpenBudgeteer.Core.Data.Services; +using Swashbuckle.AspNetCore.SwaggerGen; + +var builder = WebApplication.CreateBuilder(args); + +//builder.Services.AddDatabase(Configuration); +builder.Services.AddProblemDetails(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddTransient, ConfigureSwaggerOptions>(); +builder.Services.AddSwaggerGen(options => options.OperationFilter()); +builder.Services.AddApiVersioning(options => +{ + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = new QueryStringApiVersionReader(); +}).AddApiExplorer(options => +{ + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + options.GroupNameFormat = "'v'VVV"; +}); +builder.Services.AddDatabase(builder.Configuration); +builder.Services.AddScoped(x => + new ServiceManager(x.GetRequiredService>())); + +var app = builder.Build(); + +var versionSet = app.NewApiVersionSet() + .HasApiVersion(1, 0) + // reporting api versions will return the headers + // "api-supported-versions" and "api-deprecated-versions" + .ReportApiVersions() + .Build(); +var serviceManager = new ServiceManager(app.Services.GetRequiredService>()); + +#region AccountService + +var account = app.MapGroup( "/account" ) + .WithApiVersionSet(versionSet); +var accountService = serviceManager.AccountService; + +// GET +account.MapGet("{id:guid}", (Guid id) => accountService.Get(id)) + .MapToApiVersion(1,0); +account.MapGet("/", () => accountService.GetAll()) + .MapToApiVersion(1,0); +account.MapGet("/activeAccounts", () => accountService.GetActiveAccounts()) + .MapToApiVersion(1,0); + +// POST +account.MapPost( "/", (Account entity) => accountService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +account.MapPatch( "/", (Account entity) => accountService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +account.MapDelete( "/{id:guid}", (Guid id) => accountService.Delete(id)) + .MapToApiVersion(1,0); +account.MapDelete( "/close/{id:guid}", (Guid id) => accountService.CloseAccount(id)) + .MapToApiVersion(1,0); + +#endregion + +#region BankTransaction + +var transaction = app.MapGroup( "/transaction" ) + .WithApiVersionSet(versionSet); +var bankTransactionService = serviceManager.BankTransactionService; + +// GET +transaction.MapGet("{id:guid}", (Guid id) => bankTransactionService.Get(id)) + .MapToApiVersion(1,0); +transaction.MapGet("/", (DateTime? start, DateTime? end, int? limit) => + bankTransactionService.GetAll(start, end, limit ?? 0)) + .MapToApiVersion(1,0); +transaction.MapGet("/fromAccount/{id:guid}", (Guid id, DateTime? start, DateTime? end, int? limit) => + bankTransactionService.GetFromAccount(id, start, end, limit ?? 0)) + .MapToApiVersion(1,0); +transaction.MapGet( "/withEntities/{id:guid}", (Guid id) => + bankTransactionService.GetWithEntities(id)) + .MapToApiVersion(1,0); + +// POST +transaction.MapPost( "/", (BankTransaction entity) => bankTransactionService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); +transaction.MapPost( "/withBucket", (BankTransaction entity) => + bankTransactionService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +transaction.MapPatch( "/", (BankTransaction entity) => bankTransactionService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); +transaction.MapPatch( "/withBucket", (BankTransaction entity) => + bankTransactionService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +transaction.MapDelete( "/{id:guid}", (Guid id) => bankTransactionService.Delete(id)) + .MapToApiVersion(1,0); + +#endregion + +#region RecurringTransaction + +var recurring = app.MapGroup( "/recurring" ) + .WithApiVersionSet(versionSet); +var recurringTransactionService = serviceManager.RecurringBankTransactionService; + +// GET +recurring.MapGet("{id:guid}", (Guid id) => recurringTransactionService.Get(id)) + .MapToApiVersion(1,0); +recurring.MapGet("/", () => recurringTransactionService.GetAllWithEntities()) + .MapToApiVersion(1,0); +recurring.MapGet( "/withEntities/{id:guid}", (Guid id) => + recurringTransactionService.GetWithEntities(id)) + .MapToApiVersion(1,0); +recurring.MapGet("/pendingTransactions", async (DateTime yearMonth) => + await recurringTransactionService.GetPendingBankTransactionAsync(yearMonth)) + .MapToApiVersion(1,0); + +// POST +recurring.MapPost( "/", (RecurringBankTransaction entity) => recurringTransactionService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); +recurring.MapPost("/pendingTransactions", async (DateTime yearMonth) => + await recurringTransactionService.CreatePendingBankTransactionAsync(yearMonth)) + .MapToApiVersion(1,0); + +// PATCH +recurring.MapPatch( "/", (RecurringBankTransaction entity) => recurringTransactionService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +recurring.MapDelete( "/{id:guid}", (Guid id) => recurringTransactionService.Delete(id)) + .MapToApiVersion(1,0); + +#endregion + +#region Bucket + +var bucket = app.MapGroup( "/bucket" ) + .WithApiVersionSet(versionSet); +var bucketService = serviceManager.BucketService; + +// GET +bucket.MapGet("{id:guid}", (Guid id) => bucketService.Get(id)) + .MapToApiVersion(1,0); +bucket.MapGet("/withLatestVersion/{id:guid}", (Guid id) => bucketService.GetWithLatestVersion(id)) + .MapToApiVersion(1,0); +bucket.MapGet("/", () => bucketService.GetAll()) + .MapToApiVersion(1,0); +bucket.MapGet("/systemBuckets", () => bucketService.GetSystemBuckets()) + .MapToApiVersion(1,0); +bucket.MapGet("/activeBuckets", (DateTime? validFrom) => bucketService.GetActiveBuckets(validFrom ?? DateTime.Now)) + .MapToApiVersion(1,0); +bucket.MapGet("/getVersion/{id:guid}", (Guid id, DateTime? yearMonth) => bucketService.GetLatestVersion(id, yearMonth ?? DateTime.Now)) + .MapToApiVersion(1,0); +bucket.MapGet("/figures/{id:guid}", (Guid id, DateTime? yearMonth) => + bucketService.GetFigures(id, yearMonth ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1))) + .MapToApiVersion(1,0); +bucket.MapGet("/balance/{id:guid}", (Guid id, DateTime? yearMonth) => + bucketService.GetBalance(id, yearMonth ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1))) + .MapToApiVersion(1,0); +bucket.MapGet("/inOut/{id:guid}", (Guid id, DateTime? yearMonth) => + bucketService.GetInAndOut(id, yearMonth ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1))) + .MapToApiVersion(1,0); + +// POST +bucket.MapPost( "/", (Bucket entity) => bucketService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +bucket.MapPatch( "/", (Bucket entity) => bucketService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +bucket.MapDelete( "/{id:guid}", (Guid id) => bucketService.Delete(id)) + .MapToApiVersion(1,0); +bucket.MapDelete( "/close/{id:guid}", (Guid id, DateTime? yearMonth) => + bucketService.Close(id, yearMonth ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1))) + .MapToApiVersion(1,0); + +#endregion + +#region BucketGroup + +var group = app.MapGroup( "/group" ) + .WithApiVersionSet(versionSet); +var groupService = serviceManager.BucketGroupService; + +// GET +group.MapGet("{id:guid}", (Guid id) => groupService.GetWithBuckets(id)) + .MapToApiVersion(1,0); +group.MapGet("/", (bool full) => full ? groupService.GetAllFull() : groupService.GetAll()) + .MapToApiVersion(1,0); + +// POST +group.MapPost( "/", (BucketGroup entity) => groupService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +group.MapPatch( "/", (BucketGroup entity) => groupService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); +group.MapPatch( "/move/{id:guid}", (Guid id, int positions) => groupService.Move(id, positions)) + .MapToApiVersion(1,0); + +// DELETE +group.MapDelete( "/{id:guid}", (Guid id) => groupService.Delete(id)) + .MapToApiVersion(1,0); + +#endregion + +#region RuleSet + +var rules = app.MapGroup( "/rules" ) + .WithApiVersionSet(versionSet); +var ruleSetService = serviceManager.BucketRuleSetService; + +// GET +rules.MapGet("{id:guid}", (Guid id) => ruleSetService.Get(id)) + .MapToApiVersion(1,0); +rules.MapGet("/", () => ruleSetService.GetAll()) + .MapToApiVersion(1,0); +rules.MapGet("/mappings/{id:guid}", (Guid id) => ruleSetService.GetMappingRules(id)) + .MapToApiVersion(1,0); + +// POST +rules.MapPost( "/", (BucketRuleSet entity) => ruleSetService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +rules.MapPatch( "/", (BucketRuleSet entity) => ruleSetService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +rules.MapDelete( "/{id:guid}", (Guid id) => ruleSetService.Delete(id)) + .MapToApiVersion(1,0); + +#endregion + +#region BucketMovement + +var movement = app.MapGroup( "/movement" ) + .WithApiVersionSet(versionSet); +var movementService = serviceManager.BucketMovementService; + +// GET +movement.MapGet("{id:guid}", (Guid id) => movementService.Get(id)) + .MapToApiVersion(1,0); +movement.MapGet("/", (DateTime? periodStart, DateTime? periodEnd) => + movementService.GetAll(periodStart ?? DateTime.MinValue, periodEnd ?? DateTime.MaxValue)) + .MapToApiVersion(1,0); +movement.MapGet("/fromBucket/{id:guid}", (Guid id, DateTime? periodStart, DateTime? periodEnd) => + movementService.GetAllFromBucket(id, periodStart ?? DateTime.MinValue, periodEnd ?? DateTime.MaxValue)) + .MapToApiVersion(1,0); + +// POST +movement.MapPost( "/", (BucketMovement entity) => movementService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +movement.MapPatch( "/", (BucketMovement entity) => movementService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +movement.MapDelete( "/{id:guid}", (Guid id) => movementService.Delete(id)) + .MapToApiVersion(1,0); + +#endregion + +#region ImportProfile + +var import = app.MapGroup( "/import" ) + .WithApiVersionSet(versionSet); +var importProfileService = serviceManager.ImportProfileService; + +// GET +import.MapGet("{id:guid}", (Guid id) => importProfileService.Get(id)) + .MapToApiVersion(1,0); +import.MapGet("/", () => importProfileService.GetAll()) + .MapToApiVersion(1,0); + +// POST +import.MapPost( "/", (ImportProfile entity) => importProfileService.Create(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// PATCH +import.MapPatch( "/", (ImportProfile entity) => importProfileService.Update(entity)) + .Accepts("application/json") + .MapToApiVersion(1,0); + +// DELETE +import.MapDelete( "/{id:guid}", (Guid id) => importProfileService.Delete(id)) + .MapToApiVersion(1,0); + +#endregion + +//if (app.Environment.IsDevelopment()) +//{ + app.UseSwagger(); + app.UseSwaggerUI(options => + { + var descriptions = app.DescribeApiVersions(); + + // build a swagger endpoint for each discovered API version + foreach (var description in descriptions) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + var name = description.GroupName.ToUpperInvariant(); + options.SwaggerEndpoint(url, name); + } + }); +//} + +app.Run(); diff --git a/OpenBudgeteer.API/Properties/launchSettings.json b/OpenBudgeteer.API/Properties/launchSettings.json new file mode 100644 index 0000000..18760ef --- /dev/null +++ b/OpenBudgeteer.API/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:27997", + "sslPort": 44305 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5243", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7070;http://localhost:5243", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/OpenBudgeteer.API/SwaggerDefaultValues.cs b/OpenBudgeteer.API/SwaggerDefaultValues.cs new file mode 100644 index 0000000..5493f8e --- /dev/null +++ b/OpenBudgeteer.API/SwaggerDefaultValues.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Text.Json; + +namespace OpenBudgeteer.API; + +/// +/// Represents the OpenAPI/Swashbuckle operation filter used to document information provided, but not used. +/// +/// This is only required due to bugs in the . +/// Once they are fixed and published, this class can be removed. +public class SwaggerDefaultValues : IOperationFilter +{ + /// + public void Apply( OpenApiOperation operation, OperationFilterContext context ) + { + var apiDescription = context.ApiDescription; + + operation.Deprecated |= apiDescription.IsDeprecated(); + + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077 + foreach ( var responseType in context.ApiDescription.SupportedResponseTypes ) + { + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387 + var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(); + var response = operation.Responses[responseKey]; + + foreach ( var contentType in response.Content.Keys ) + { + if ( !responseType.ApiResponseFormats.Any( x => x.MediaType == contentType ) ) + { + response.Content.Remove( contentType ); + } + } + } + + if ( operation.Parameters == null ) + { + return; + } + + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 + foreach ( var parameter in operation.Parameters ) + { + var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); + + parameter.Description ??= description.ModelMetadata?.Description; + + if ( parameter.Schema.Default == null && + description.DefaultValue != null && + description.DefaultValue is not DBNull && + description.ModelMetadata is ModelMetadata modelMetadata ) + { + // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330 + var json = JsonSerializer.Serialize( description.DefaultValue, modelMetadata.ModelType ); + parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json ); + } + + parameter.Required |= description.IsRequired; + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/App.razor b/OpenBudgeteer.Blazor/App.razor index bf6c773..d153e43 100644 --- a/OpenBudgeteer.Blazor/App.razor +++ b/OpenBudgeteer.Blazor/App.razor @@ -1,11 +1,67 @@ - - - - - - - -

Sorry, there's nothing at this address.

-
-
-
+@using Microsoft.Extensions.Hosting +@using OpenBudgeteer.Core.Common +@inject IHostEnvironment Env + + + + + + + OpenBudgeteer + + + + + + + + + + + +
+ @if (Env.IsDevelopment()) + { + + An unhandled exception has occurred. See browser dev tools for details. + + } + else + { + + An error has occurred. This app may no longer respond until reloaded. Please check logs and submit a bug report on GitHub. + + } + Reload +
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/HostedDatabaseMigrator.cs b/OpenBudgeteer.Blazor/HostedDatabaseMigrator.cs index 246fec4..45bc337 100644 --- a/OpenBudgeteer.Blazor/HostedDatabaseMigrator.cs +++ b/OpenBudgeteer.Blazor/HostedDatabaseMigrator.cs @@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using OpenBudgeteer.Data; -using OpenBudgeteer.Data.Initialization; -using OpenBudgeteer.Data.OnlineChecker; +using OpenBudgeteer.Core.Data.Entities; +using OpenBudgeteer.Core.Data.Initialization; +using OpenBudgeteer.Core.Data.OnlineChecker; namespace OpenBudgeteer.Blazor; @@ -16,6 +16,8 @@ public class HostedDatabaseMigrator : IHostedService private readonly IConfiguration _configuration; private readonly IDatabaseInitializer _databaseInitializer; private readonly IDatabaseOnlineChecker _onlineChecker; + + private const string APPSETTINGS_DEMO_DATA = "APPSETTINGS_DEMO_DATA"; public HostedDatabaseMigrator( DbContextOptions dbContextOptions, @@ -43,6 +45,9 @@ public async Task StartAsync(CancellationToken cancellationToken) await using var context = new DatabaseContext(_dbContextOptions); await context.Database.MigrateAsync(cancellationToken: cancellationToken); + + var initializeWithDemoData = _configuration.GetValue(APPSETTINGS_DEMO_DATA); + if (initializeWithDemoData) new DemoDataGenerator(_dbContextOptions).GenerateDemoData(); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index e5a2716..8f6c401 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -1,30 +1,31 @@  - net7.0 + net8.0 c146cdfd-f78c-4fb3-8be0-4e15e589371a false Linux OpenBudgeteer + enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - - - + + + diff --git a/OpenBudgeteer.Blazor/Pages/Account.razor b/OpenBudgeteer.Blazor/Pages/Account.razor index 4972068..7bcf449 100644 --- a/OpenBudgeteer.Blazor/Pages/Account.razor +++ b/OpenBudgeteer.Blazor/Pages/Account.razor @@ -1,31 +1,21 @@ @page "/account" -@using OpenBudgeteer.Core.ViewModels -@using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Core.ViewModels.ItemViewModels @using System.Globalization -@using OpenBudgeteer.Core.Common -@using OpenBudgeteer.Data -@inject DbContextOptions DbContextOptions -
-
- -
+
+
-
+
@foreach (var account in _dataContext.Accounts) { -
-
-
-
@account.Account.Name
-

Balance: @account.Balance.ToString("C", CultureInfo.CurrentCulture)

- - - -
+ } @@ -35,8 +25,8 @@ Title="@_editAccountDialogTitle" DataContext="@_editAccountDialogDataContext" IsDialogVisible="@_isEditAccountModalDialogVisible" - OnSaveClickCallback="@(() => SaveChanges(_editAccountDialogDataContext))" - OnCancelClickCallback="@(() => CancelChanges())"/> + OnSaveClickCallback="@(() => SaveChanges(_editAccountDialogDataContext!))" + OnCancelClickCallback="@(CancelChanges)"/> - -@code { - AccountViewModel _dataContext; - TransactionViewModel _transactionModalDialogDataContext; - - bool _isEditAccountModalDialogVisible; - string _editAccountDialogTitle; - AccountViewModelItem _editAccountDialogDataContext; - - - bool _isTransactionModalDialogVisible; - bool _isTransactionModalDialogDataContextLoading; - - bool _isErrorModalDialogVisible; - string _errorModalDialogMessage; - - protected override void OnInitialized() - { - _dataContext = new AccountViewModel(DbContextOptions); - HandleResult(_dataContext.LoadData()); - } - - private void CreateNewAccount() - { - _editAccountDialogTitle = "New Account"; - _editAccountDialogDataContext = _dataContext.PrepareNewAccount(); - _isEditAccountModalDialogVisible = true; - } - - private void EditAccount(AccountViewModelItem account) - { - _editAccountDialogTitle = "Edit Account"; - _editAccountDialogDataContext = account; - _isEditAccountModalDialogVisible = true; - } - - - private void SaveChanges(AccountViewModelItem account) - { - _isEditAccountModalDialogVisible = false; - HandleResult(account.CreateUpdateAccount()); - } - - private void CancelChanges() - { - _isEditAccountModalDialogVisible = false; - HandleResult(_dataContext.LoadData()); - } - - private void CloseAccount(AccountViewModelItem account) - { - HandleResult(account.CloseAccount()); - } - - void HandleResult(ViewModelOperationResult result) - { - if (!result.IsSuccessful) - { - _errorModalDialogMessage = result.Message; - _isErrorModalDialogVisible = true; - } - if (result.ViewModelReloadRequired) - { - _dataContext.LoadData(); - StateHasChanged(); - } - } - - async void DisplayAccountTransactions(AccountViewModelItem account) - { - _isTransactionModalDialogVisible = true; - _isTransactionModalDialogDataContextLoading = true; - - _transactionModalDialogDataContext = new TransactionViewModel(DbContextOptions, new YearMonthSelectorViewModel()); - HandleResult(await _transactionModalDialogDataContext.LoadDataAsync(account.Account)); - - _isTransactionModalDialogDataContextLoading = false; - StateHasChanged(); - } -} + \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Account.razor.cs b/OpenBudgeteer.Blazor/Pages/Account.razor.cs new file mode 100644 index 0000000..73b791d --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Account.razor.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Components; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.ViewModels.EntityViewModels; +using OpenBudgeteer.Core.ViewModels.Helper; +using OpenBudgeteer.Core.ViewModels.PageViewModels; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Account : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + + private AccountPageViewModel _dataContext = null!; + private TransactionListingViewModel? _transactionModalDialogDataContext; + + private bool _isEditAccountModalDialogVisible; + private string _editAccountDialogTitle = string.Empty; + private AccountViewModel? _editAccountDialogDataContext; + + private bool _isTransactionModalDialogVisible; + private bool _isTransactionModalDialogDataContextLoading; + + private bool _isErrorModalDialogVisible; + private string _errorModalDialogMessage = string.Empty; + + protected override void OnInitialized() + { + _dataContext = new AccountPageViewModel(ServiceManager); + HandleResult(_dataContext.LoadData()); + } + + private void CreateNewAccount() + { + _editAccountDialogTitle = "New Account"; + _editAccountDialogDataContext = AccountViewModel.CreateEmpty(ServiceManager); + _isEditAccountModalDialogVisible = true; + } + + private void EditAccount(AccountViewModel account) + { + _editAccountDialogTitle = "Edit Account"; + _editAccountDialogDataContext = account; + _isEditAccountModalDialogVisible = true; + } + + + private void SaveChanges(AccountViewModel account) + { + _isEditAccountModalDialogVisible = false; + HandleResult(account.CreateOrUpdateAccount()); + } + + private void CancelChanges() + { + _isEditAccountModalDialogVisible = false; + HandleResult(_dataContext.LoadData()); + } + + private void CloseAccount(AccountViewModel account) + { + HandleResult(account.CloseAccount()); + } + + private void HandleResult(ViewModelOperationResult result) + { + if (!result.IsSuccessful) + { + _errorModalDialogMessage = result.Message; + _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + _dataContext.LoadData(); + StateHasChanged(); + } + } + + private async void DisplayAccountTransactions(AccountViewModel account) + { + _isTransactionModalDialogVisible = true; + _isTransactionModalDialogDataContextLoading = true; + + _transactionModalDialogDataContext = new TransactionListingViewModel(ServiceManager); + HandleResult(await _transactionModalDialogDataContext.LoadDataAsync(account.AccountId)); + + _isTransactionModalDialogDataContextLoading = false; + StateHasChanged(); + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Bucket.razor b/OpenBudgeteer.Blazor/Pages/Bucket.razor index f740141..b35cfcb 100644 --- a/OpenBudgeteer.Blazor/Pages/Bucket.razor +++ b/OpenBudgeteer.Blazor/Pages/Bucket.razor @@ -1,140 +1,76 @@ @page "/bucket" -@using OpenBudgeteer.Blazor.ViewModels -@using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.ViewModels.ItemViewModels -@using Microsoft.EntityFrameworkCore @using System.Drawing @using System.Globalization -@using OpenBudgeteer.Contracts.Models -@using OpenBudgeteer.Core.Common -@using OpenBudgeteer.Data -@inject DbContextOptions DbContextOptions -@inject YearMonthSelectorViewModel YearMonthDataContext - -
-
-
-
- -
-
- Income -
- @_dataContext.Income.ToString("C", CultureInfo.CurrentCulture) -
-
-
-
-
-
- -
-
- Expenses -
- @_dataContext.Expenses.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
+
-
-
-
- -
-
- Month Balance -
- @_dataContext.MonthBalance.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
-
-
-
- -
-
- Bank Balance -
- @_dataContext.BankBalance.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
-
-
+
+
-
-
-
- -
-
- Budget -
- @_dataContext.Budget.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
+
+
+
+
+
-
-
-
- -
-
- Pending Want -
- @_dataContext.PendingWant.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
-
-
-
- -
-
- Remaining Budget -
- @_dataContext.RemainingBudget.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
-
-
-
- -
-
- Negative Bucket Balance -
- @_dataContext.NegativeBucketBalance.ToString("C", CultureInfo.CurrentCulture) -
-
+
+
-
-
- - - - -
- +
+ + + + +
@@ -143,140 +79,133 @@ - - - - - + + + + + - -
Bucket Balance InOutWantInActivityDetailsWantInActivityDetails
- -@foreach (var bucketGroup in _dataContext.BucketGroups) -{ - - - + + @foreach (var bucketGroup in _dataContext.BucketGroups) + { + @if (bucketGroup.InModification) { - } else { - - - - - - + + + + + + + + } - - -
+
- +
- +
- +
+
- - @bucketGroup.BucketGroup.Name + @bucketGroup.Name
- -
@bucketGroup.TotalBalance.ToString("N2")@(bucketGroup.TotalWant == 0 ? string.Empty : bucketGroup.TotalWant.ToString("N2"))@(bucketGroup.TotalIn == 0 ? string.Empty : bucketGroup.TotalIn.ToString("N2"))@(bucketGroup.TotalActivity == 0 ? string.Empty : bucketGroup.TotalActivity.ToString("N2")) - @if (bucketGroup.IsHovered) - { - - - - - - } - @bucketGroup.TotalBalance.ToString("C", CultureInfo.CurrentCulture)@(bucketGroup.TotalWant == 0 ? string.Empty : bucketGroup.TotalWant.ToString("C", CultureInfo.CurrentCulture))@(bucketGroup.TotalIn == 0 ? string.Empty : bucketGroup.TotalIn.ToString("C", CultureInfo.CurrentCulture))@(bucketGroup.TotalActivity == 0 ? string.Empty : bucketGroup.TotalActivity.ToString("C", CultureInfo.CurrentCulture)) + + + + + +
- - - - @foreach (var bucket in bucketGroup.Buckets) - { - - + +
- @if (bucket.Bucket.IsInactive) +
+ + + @foreach (var bucket in bucketGroup.Buckets) { -
- @($"{bucket.Bucket.Name} (Inactive from: {bucket.Bucket.IsInactiveFrom.ToShortDateString()})") -
- } - else - { -
@bucket.Bucket.Name
- } - - - - - - - + - + + + + + + + + } - - - } - -
@bucket.Balance.ToString("N2") - @if (bucket.Bucket.IsInactive) - { - - } - else - { - - } - @(bucket.Want == 0 ? string.Empty : bucket.Want.ToString("N2"))@(bucket.In == 0 ? string.Empty : bucket.In.ToString("N2"))@(bucket.Activity == 0 ? string.Empty : bucket.Activity.ToString("N2")) - @if (bucket.IsProgressbarVisible) - { -
-
-
-
- @bucket.Progress% +
+ @if (bucket.IsInactive) + { +
+ @($"{bucket.Name} (Inactive from: {bucket.IsInactiveFrom.ToShortDateString()})")
- - - -
-
- @bucket.Details -
-
- } -
- @if (bucket.IsHovered) - { - if (bucket.Bucket.IsInactive) - { - - } - else - { - - - - } + } + else + { +
@bucket.Name
+ } +
@bucket.Balance.ToString("C", CultureInfo.CurrentCulture) + @if (bucket.IsInactive) + { + + } + else + { + + } + @(bucket.Want == 0 ? string.Empty : bucket.Want.ToString("C", CultureInfo.CurrentCulture))@(bucket.In == 0 ? string.Empty : bucket.In.ToString("C", CultureInfo.CurrentCulture))@(bucket.Activity == 0 ? string.Empty : bucket.Activity.ToString("C", CultureInfo.CurrentCulture)) + @if (bucket.IsProgressbarVisible) + { +
+
+
+
+ @bucket.Progress% +
+
+
+
+
+
+ @bucket.Details +
+
+ } +
+ @if (bucket.IsInactive) + { + + } + else + { + + + + } +
-} +
+ + + } + + + + - -@code { - BucketViewModel _dataContext; - - BucketGroup _newBucketGroupDialogDataContext; - bool _isNewBucketGroupModalDialogVisible; - - BucketViewModelItem _editBucketDialogDataContext; - bool _isEditBucketModalDialogVisible; - - BlazorBucketStatisticsViewModel _bucketDetailsModalDialogDataContext; - bool _isBucketDetailsModalDialogVisible; - bool _isBucketDetailsModalDialogDataContextLoading; - - bool _isCloseBucketDialogVisible; - BucketViewModelItem _bucketToBeClosed; - - bool _isErrorModalDialogVisible; - string _errorModalDialogMessage; - bool _hasErrorInBucketModalDialog; - - protected override async Task OnInitializedAsync() - { - _dataContext = new BucketViewModel(DbContextOptions, YearMonthDataContext); - - await HandleResult(await _dataContext.LoadDataAsync()); - - YearMonthDataContext.SelectedYearMonthChanged += async (sender, args) => - { - await HandleResult(await _dataContext.LoadDataAsync()); - StateHasChanged(); - }; - } - - async void DistributeBudget() - { - await HandleResult(_dataContext.DistributeBudget()); - } - - void CreateBucket(BucketGroupViewModelItem bucketGroup) - { - var newBucket = bucketGroup.CreateBucket(); - ShowEditBucketDialog(newBucket); - } - - void ShowNewBucketGroupDialog() - { - _newBucketGroupDialogDataContext = new BucketGroup - { - BucketGroupId = Guid.Empty, - Name = string.Empty, - Position = 1 - }; - _isNewBucketGroupModalDialogVisible = true; - } - - async void SaveAndCloseNewBucketGroupDialog() - { - _isNewBucketGroupModalDialogVisible = false; - // Requested Position is last, so set right position number - if (_newBucketGroupDialogDataContext.Position == -1) - _newBucketGroupDialogDataContext.Position = _dataContext.BucketGroups.Count + 1; - await HandleResult(_dataContext.CreateGroup(_newBucketGroupDialogDataContext)); - } - - void CancelNewBucketGroupDialog() - { - _isNewBucketGroupModalDialogVisible = false; - } - - void ShowEditBucketDialog(BucketViewModelItem bucket) - { - _editBucketDialogDataContext = bucket; - _isEditBucketModalDialogVisible = true; - } - - async void SaveAndCloseEditBucketDialog() - { - _isEditBucketModalDialogVisible = false; - var result = _dataContext.SaveChanges(_editBucketDialogDataContext); - await HandleResult(result); - if (!result.IsSuccessful) - { - _hasErrorInBucketModalDialog = true; // Ensures that Dialog will be displayed again - return; // Error message is shown in HandleResult() - } - StateHasChanged(); - } - - async void CancelEditBucketDialog() - { - _isEditBucketModalDialogVisible = false; - await HandleResult(await _dataContext.LoadDataAsync()); - StateHasChanged(); - } - - void HandleBucketCloseRequest(BucketViewModelItem bucket) - { - _bucketToBeClosed = bucket; - _isCloseBucketDialogVisible = true; - } - - void CancelCloseBucket() - { - _isCloseBucketDialogVisible = false; - _bucketToBeClosed = null; - } - - async void CloseBucket() - { - _isCloseBucketDialogVisible = false; - await HandleResult(_dataContext.CloseBucket(_bucketToBeClosed)); - StateHasChanged(); - } - - async Task HandleResult(ViewModelOperationResult result) - { - if (!result.IsSuccessful) - { - _errorModalDialogMessage = result.Message; - _isErrorModalDialogVisible = true; - } - if (result.ViewModelReloadRequired) - { - await _dataContext.LoadDataAsync(); - StateHasChanged(); - } - } - - async void InOut_Changed(BucketViewModelItem bucket, KeyboardEventArgs args) - { - if (args.Key != "Enter") return; - var result = bucket.HandleInOutInput(); - if (result.IsSuccessful) - { - await HandleResult(_dataContext.UpdateBalanceFigures()); - StateHasChanged(); - } - else - { - await HandleResult(result); - } - } - - async void DisplayBucketDetails(BucketViewModelItem bucket) - { - _isBucketDetailsModalDialogVisible = true; - _isBucketDetailsModalDialogDataContextLoading = true; - - _bucketDetailsModalDialogDataContext = new BlazorBucketStatisticsViewModel(DbContextOptions, YearMonthDataContext, bucket.Bucket); - await _bucketDetailsModalDialogDataContext.LoadDataAsync(true); - - _isBucketDetailsModalDialogDataContextLoading = false; - StateHasChanged(); - } - - void CloseErrorDialog() - { - _isErrorModalDialogVisible = false; - // In case error occuring in EditBucketDialog, display it again - if (_hasErrorInBucketModalDialog) - { - _isEditBucketModalDialogVisible = true; - _hasErrorInBucketModalDialog = false; - } - } -} diff --git a/OpenBudgeteer.Blazor/Pages/Bucket.razor.cs b/OpenBudgeteer.Blazor/Pages/Bucket.razor.cs new file mode 100644 index 0000000..75063fa --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Bucket.razor.cs @@ -0,0 +1,196 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using OpenBudgeteer.Blazor.ViewModels; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.ViewModels.EntityViewModels; +using OpenBudgeteer.Core.ViewModels.Helper; +using OpenBudgeteer.Core.ViewModels.PageViewModels; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Bucket : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + [Inject] private YearMonthSelectorViewModel YearMonthDataContext { get; set; } = null!; + + private BucketPageViewModel _dataContext = null!; + + private bool _isNewBucketGroupModalDialogVisible; + + private BucketViewModel? _editBucketDialogDataContext; + private bool _isEditBucketModalDialogVisible; + + private BlazorBucketStatisticsViewModel? _bucketDetailsModalDialogDataContext; + private bool _isBucketDetailsModalDialogVisible; + private bool _isBucketDetailsModalDialogDataContextLoading; + + private bool _isDeleteBucketGroupDialogVisible; + private BucketGroupViewModel? _bucketGroupToBeDeleted; + + private bool _isCloseBucketDialogVisible; + private BucketViewModel? _bucketToBeClosed; + + private bool _isErrorModalDialogVisible; + private string _errorModalDialogMessage = string.Empty; + private bool _hasErrorInBucketModalDialog; + + protected override async Task OnInitializedAsync() + { + _dataContext = new BucketPageViewModel(ServiceManager, YearMonthDataContext); + + await HandleResult(await _dataContext.LoadDataAsync()); + + YearMonthDataContext.SelectedYearMonthChanged += async (sender, args) => + { + await HandleResult(await _dataContext.LoadDataAsync()); + StateHasChanged(); + }; + } + + private async void DistributeBudget() + { + await HandleResult(_dataContext.DistributeBudget()); + } + + private void CreateBucket(BucketGroupViewModel bucketGroup) + { + var newBucket = bucketGroup.CreateBucket(); + ShowEditBucketDialog(newBucket); + } + + private void ShowNewBucketGroupDialog() + { + _dataContext.CreateEmptyGroup(); + _isNewBucketGroupModalDialogVisible = true; + } + + private async void SaveAndCloseNewBucketGroupDialog() + { + _isNewBucketGroupModalDialogVisible = false; + await HandleResult(_dataContext.NewBucketGroup!.CreateGroup()); + } + + private void CancelNewBucketGroupDialog() + { + _isNewBucketGroupModalDialogVisible = false; + } + + private void HandleBucketGroupDeleteRequest(BucketGroupViewModel bucketGroup) + { + _bucketGroupToBeDeleted = bucketGroup; + _isDeleteBucketGroupDialogVisible = true; + } + + private void CancelDeleteBucketGroup() + { + _isDeleteBucketGroupDialogVisible = false; + _bucketGroupToBeDeleted = null; + } + + private async void DeleteBucketGroup() + { + _isDeleteBucketGroupDialogVisible = false; + if(_bucketGroupToBeDeleted != null) await HandleResult(_bucketGroupToBeDeleted.DeleteGroup()); + _bucketGroupToBeDeleted = null; + StateHasChanged(); + } + + private void ShowEditBucketDialog(BucketViewModel bucket) + { + _editBucketDialogDataContext = bucket; + _isEditBucketModalDialogVisible = true; + } + + private async void SaveAndCloseEditBucketDialog() + { + _isEditBucketModalDialogVisible = false; + var result = _dataContext.SaveChanges(_editBucketDialogDataContext!); + await HandleResult(result); + if (!result.IsSuccessful) + { + _hasErrorInBucketModalDialog = true; // Ensures that Dialog will be displayed again + return; // Error message is shown in HandleResult() + } + StateHasChanged(); + } + + private async void CancelEditBucketDialog() + { + _isEditBucketModalDialogVisible = false; + await HandleResult(await _dataContext.LoadDataAsync()); + StateHasChanged(); + } + + private void HandleBucketCloseRequest(BucketViewModel bucket) + { + _bucketToBeClosed = bucket; + _isCloseBucketDialogVisible = true; + } + + private void CancelCloseBucket() + { + _isCloseBucketDialogVisible = false; + _bucketToBeClosed = null; + } + + private async void CloseBucket() + { + _isCloseBucketDialogVisible = false; + await HandleResult(_dataContext.CloseBucket(_bucketToBeClosed!)); + StateHasChanged(); + } + + private async Task HandleResult(ViewModelOperationResult result) + { + if (!result.IsSuccessful) + { + _errorModalDialogMessage = result.Message; + _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + await _dataContext.LoadDataAsync(); + StateHasChanged(); + } + } + + private async void InOut_Changed(BucketViewModel bucket, KeyboardEventArgs args) + { + if (args.Key != "Enter") return; + var result = bucket.HandleInOutInput(); + if (result.IsSuccessful) + { + await HandleResult(_dataContext.UpdateBalanceFigures()); + StateHasChanged(); + } + else + { + await HandleResult(result); + } + } + + private async void DisplayBucketDetails(BucketViewModel bucket) + { + _isBucketDetailsModalDialogVisible = true; + _isBucketDetailsModalDialogDataContextLoading = true; + + _bucketDetailsModalDialogDataContext = new BlazorBucketStatisticsViewModel(ServiceManager, YearMonthDataContext, bucket.BucketId); + await _bucketDetailsModalDialogDataContext.LoadDataAsync(true); + + _isBucketDetailsModalDialogDataContextLoading = false; + StateHasChanged(); + } + + private void CloseErrorDialog() + { + _isErrorModalDialogVisible = false; + // In case error occuring in EditBucketDialog, display it again + if (_hasErrorInBucketModalDialog) + { + _isEditBucketModalDialogVisible = true; + _hasErrorInBucketModalDialog = false; + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/DataConsistency.razor b/OpenBudgeteer.Blazor/Pages/DataConsistency.razor index e536970..2fd147c 100644 --- a/OpenBudgeteer.Blazor/Pages/DataConsistency.razor +++ b/OpenBudgeteer.Blazor/Pages/DataConsistency.razor @@ -1,11 +1,5 @@ @page "/dataconsistency" - -@using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Contracts.Models -@using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Data - -@inject DbContextOptions DbContextOptions +@using OpenBudgeteer.Core.Common @@ -75,33 +69,8 @@
} -@if (_isLoadingDialogVisible) -{ - - -} - -@code { - DataConsistencyViewModel _dataContext; - - bool _isLoadingDialogVisible; - - protected override async Task OnInitializedAsync() - { - _isLoadingDialogVisible = true; - StateHasChanged(); - _dataContext = new DataConsistencyViewModel(DbContextOptions); - await _dataContext.RunAllChecksAsync(); - _isLoadingDialogVisible = false; - } -} + diff --git a/OpenBudgeteer.Blazor/Pages/DataConsistency.razor.cs b/OpenBudgeteer.Blazor/Pages/DataConsistency.razor.cs new file mode 100644 index 0000000..d06739b --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/DataConsistency.razor.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.ViewModels.PageViewModels; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class DataConsistency : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + + DataConsistencyPageViewModel _dataContext = null!; + + private bool _isLoadingDialogVisible; + + protected override async Task OnInitializedAsync() + { + _isLoadingDialogVisible = true; + StateHasChanged(); + _dataContext = new DataConsistencyPageViewModel(ServiceManager); + await _dataContext.RunAllChecksAsync(); + _isLoadingDialogVisible = false; + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index d83d4ec..70a0105 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -1,21 +1,12 @@ @page "/import" - -@using OpenBudgeteer.Core.ViewModels -@using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Contracts.Models -@using OpenBudgeteer.Core.Common -@using OpenBudgeteer.Data @using Tewr.Blazor.FileReader -@using Microsoft.AspNetCore.WebUtilities -@using Microsoft.AspNetCore.Components -@using System.IO -@using System.Text -@inject DbContextOptions DbContextOptions +@using OpenBudgeteer.Core.ViewModels.EntityViewModels +@using System.Globalization @inject IFileReaderService FileReaderService @inject IJSRuntime JSRuntime @inject NavigationManager NavManager -
+

-
+
@if (_step4Enabled) { - + } @if (_dataContext.SelectedImportProfile.ImportProfileId != Guid.Empty) { - - + + }
-
-
+
+
-
+
-
+
-
-
+
+
-
- - -
-
+

e.g. yyyy-MM-dd, dd.MM.yyyy, MM/dd/yyyy

-
+
+
+
+ + +
+

Use BCP 47 language tag like en-US or de-DE

-
+
@@ -138,45 +131,45 @@
-
-
+
+
-
+
-
+
-
+
-
Additional Mapping Settings:
-
-
-
- - -
-
- - -
-
- - -
-
+
Additional Mapping Settings:
+
+ + +
+
+ + +
+
+ + +
+
@switch (_dataContext.SelectedImportProfile.AdditionalSettingCreditValue) { - case 1: -
+ case ImportProfileViewModel.AdditionalSettingsForCreditValues.CreditInSeparateColumns: +
break; - case 2: -
+ case ImportProfileViewModel.AdditionalSettingsForCreditValues.DebitCreditAlwaysPositive: +
-
+
break; - case 0: + case ImportProfileViewModel.AdditionalSettingsForCreditValues.NoSettings: default: break; } @@ -247,7 +238,7 @@
-
+
@@ -255,7 +246,7 @@
@if (_dataContext.SelectedImportProfile.AdditionalSettingAmountCleanup) { -
+
@@ -279,34 +270,36 @@
- @if (!_isValidationRunning) - { - - } - else - { - - } - - @if (_dataContext.ValidRecords > 0 && !_isValidationRunning) - { - @if (!_isImportRunning) +
+ @if (!_isValidationRunning) { - - + } else { - + Validating... + } - } -
+ + @if (_dataContext.ValidRecords > 0 && !_isValidationRunning) + { + @if (!_isImportRunning) + { + + + } + else + { + + } + } +
+
Total Records: @_dataContext.TotalRecords
Valid Records: @_dataContext.ValidRecords
Records with errors: @_dataContext.RecordsWithErrors
@@ -319,14 +312,14 @@ @if (_dataContext.ParsedRecords.Any(i => i.IsValid)) { -
+
Preview (Valid Records)
- - + + @@ -336,20 +329,20 @@ { - - + + - + }
DateAccountPayeeAccountPayee Memo Amount
@transaction.Result.TransactionDate.ToShortDateString()@_dataContext.SelectedAccount.Name@transaction.Result.Payee@_dataContext.SelectedImportProfile.Account.Name@transaction.Result.Payee @transaction.Result.Memo@transaction.Result.Amount@transaction.Result.Amount.ToString("C", CultureInfo.CurrentCulture)
-
+
} - + @if (_dataContext.ParsedRecords.Any(i => !i.IsValid)) { -
+
Records with error:
@@ -369,46 +362,46 @@ }
-
+
} - + @if (_dataContext.Duplicates.Any()) {
Potential Duplicates:
- - - - - - - - + + + + + + + + - @foreach (var duplicate in _dataContext.Duplicates) + @foreach (var duplicate in _dataContext.Duplicates) + { + + + + + + + + + foreach (var bankTransaction in duplicate.Item2) { - - - - - - - + + + + + + - foreach (var bankTransaction in duplicate.Item2) - { - - - - - - - - } } + }
DateAccountPayeeMemoAmount
DateAccountPayeeMemoAmount
@duplicate.Item1.Result.TransactionDate.ToShortDateString()@_dataContext.SelectedImportProfile.Account.Name@duplicate.Item1.Result.Payee@duplicate.Item1.Result.Memo@duplicate.Item1.Result.Amount.ToString("C", CultureInfo.CurrentCulture)
@duplicate.Item1.Result.TransactionDate.ToShortDateString()@_dataContext.SelectedAccount.Name@duplicate.Item1.Result.Payee@duplicate.Item1.Result.Memo@duplicate.Item1.Result.Amount
@bankTransaction.TransactionDate.ToShortDateString()@_dataContext.SelectedImportProfile.Account.Name@bankTransaction.Payee@bankTransaction.Memo@bankTransaction.Amount.ToString("C", CultureInfo.CurrentCulture)
@bankTransaction.TransactionDate.ToShortDateString()@_dataContext.SelectedAccount.Name@bankTransaction.Payee@bankTransaction.Memo@bankTransaction.Amount
@@ -420,7 +413,7 @@
File Content:
- +
@if (_isConfirmationModalDialogVisible) @@ -449,275 +442,18 @@ Title="Import" Message="@_infoDialogMessage" IsDialogVisible="@_isInfoDialogVisible" - OnCloseClickCallback="@(() => _isInfoDialogVisible = false)" - /> + IsInteractionEnabled="@_isInfoDialogInteractionEnabled" + OnCloseClickCallback="@(() => _isInfoDialogVisible = false)"/> + OnCancelClickCallback="@(() => _isDeleteConfirmationDialogVisible = false)"/> - -@code { - ImportDataViewModel _dataContext; - - ElementReference _inputElement; - ElementReference _step1AccordionButtonElement; - ElementReference _step4AccordionButtonElement; - - readonly Guid PlaceholderItemId = Guid.Parse("11111111-1111-1111-1111-111111111111"); - readonly string PlaceholderItemValue = "___PlaceholderItem___"; - readonly string DummyColumn = "---Select Column---"; - - readonly ImportProfile _dummyImportProfile = new() - { - ImportProfileId = Guid.Parse("11111111-1111-1111-1111-111111111111"), - ProfileName = "---Select Import Profile---", - AccountId = Guid.Parse("11111111-1111-1111-1111-111111111111") - }; - - readonly Contracts.Models.Account _dummyAccount = new() - { - AccountId = Guid.Parse("11111111-1111-1111-1111-111111111111"), - Name = "---Select Target Account---" - }; - - bool _step2Enabled; - bool _step3Enabled; - bool _step4Enabled; - bool _forceShowStep1; - bool _forceShowStep4; - - bool _isValidationRunning; - bool _isImportRunning; - - string _validationErrorMessage = string.Empty; - - bool _isConfirmationModalDialogVisible; - string _importConfirmationMessage; - - bool _isInfoDialogVisible; - string _infoDialogMessage; - - bool _isDeleteConfirmationDialogVisible; - - bool _isErrorModalDialogVisible; - string _errorModalDialogMessage; - - protected override void OnInitialized() - { - _dataContext = new ImportDataViewModel(DbContextOptions); - LoadData(); - LoadFromQueryParams(); - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - if (_forceShowStep1) - { - _forceShowStep1 = false; - await JSRuntime.InvokeVoidAsync("ImportPage.triggerClick", _step1AccordionButtonElement); - } - if (_forceShowStep4) - { - _forceShowStep4 = false; - await JSRuntime.InvokeVoidAsync("ImportPage.triggerClick", _step4AccordionButtonElement); - } - } - - void LoadData() - { - HandleResult(_dataContext.LoadData()); - _dataContext.AvailableImportProfiles.Insert(0, _dummyImportProfile); - _dataContext.AvailableAccounts.Insert(0, _dummyAccount); - _dataContext.SelectedImportProfile = _dummyImportProfile; - _dataContext.SelectedAccount = _dummyAccount; - } - - async void LoadFromQueryParams() - { - var uri = NavManager.ToAbsoluteUri(NavManager.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); - - if (query.TryGetValue("csv", out var csv64)) - { - HandleResult(await LoadCsvFromBase64StringAsync(csv64)); - } - - if (_step2Enabled && query.TryGetValue("profile", out var profileName)) - { - var profile = _dataContext.AvailableImportProfiles.FirstOrDefault(i => i.ProfileName == profileName, _dummyImportProfile); - _dataContext.SelectedImportProfile = profile; - ImportProfile_SelectionChanged(); - } - - if (_step4Enabled) - { - await ValidateDataAsync(); - _forceShowStep4 = true; - StateHasChanged(); - } - } - - async Task LoadCsvFromBase64StringAsync(string csv64) - { - try - { - var csv = Encoding.UTF8.GetString(Convert.FromBase64String(csv64)); - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - await writer.WriteAsync(csv); - await writer.FlushAsync(); - stream.Position = 0; - var res = await _dataContext.HandleOpenFileAsync(stream); - if (res.IsSuccessful) - { - _step2Enabled = true; - } - return res; - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Failed to load CSV: {e.Message}"); - } - } - - async Task ReadFileAsync() - { - _step2Enabled = false; - _step3Enabled = false; - _step4Enabled = false; - _dataContext.SelectedImportProfile = _dummyImportProfile; - _dataContext.SelectedAccount = _dummyAccount; - - var file = (await FileReaderService.CreateReference(_inputElement).EnumerateFilesAsync()).FirstOrDefault(); - if (file == null) return; - HandleResult(await _dataContext.HandleOpenFileAsync(await file.OpenReadAsync())); - _step2Enabled = true; - } - - void LoadProfile() - { - _dataContext.InitializeDataFromImportProfile(); - _step3Enabled = - _dataContext.SelectedImportProfile.ImportProfileId != Guid.Empty && - _dataContext.SelectedImportProfile.ImportProfileId != PlaceholderItemId; - _dataContext.IdentifiedColumns.Insert(0, DummyColumn); - CheckColumnMapping(); - StateHasChanged(); - } - - void DeleteProfile() - { - _isDeleteConfirmationDialogVisible = false; - HandleResult(_dataContext.DeleteProfile()); - if (_dataContext.SelectedImportProfile.ImportProfileId == Guid.Empty || - _dataContext.SelectedImportProfile.ImportProfileId == PlaceholderItemId) - { - _dataContext.SelectedImportProfile = _dummyImportProfile; - _dataContext.AvailableImportProfiles.Insert(0, _dummyImportProfile); - _dataContext.SelectedAccount = _dummyAccount; - } - } - - void LoadHeaders() - { - var result = _dataContext.LoadHeaders(); - if (result.IsSuccessful) - { - _dataContext.IdentifiedColumns.Insert(0, DummyColumn); - _step3Enabled = true; - } - else - { - HandleResult(result); - } - } - - void CheckColumnMapping() - { - _step4Enabled = false; - if (string.IsNullOrEmpty(_dataContext.SelectedImportProfile.TransactionDateColumnName) || - _dataContext.SelectedImportProfile.TransactionDateColumnName == PlaceholderItemValue) return; - // Make Payee optional - //if (string.IsNullOrEmpty(_dataContext.PayeeColumn) || _dataContext.PayeeColumn == _placeholderItemValue) return; - if (string.IsNullOrEmpty(_dataContext.SelectedImportProfile.MemoColumnName) || - _dataContext.SelectedImportProfile.MemoColumnName == PlaceholderItemValue) return; - if (string.IsNullOrEmpty(_dataContext.SelectedImportProfile.AmountColumnName) || - _dataContext.SelectedImportProfile.AmountColumnName == PlaceholderItemValue) return; - _step4Enabled = true; - } - - async Task ValidateDataAsync() - { - _isValidationRunning = true; - _dataContext.IdentifiedColumns.Remove(DummyColumn); // Remove DummyColumn to prevent wrong column index - _validationErrorMessage = (await _dataContext.ValidateDataAsync()).Message; - _dataContext.IdentifiedColumns.Insert(0, DummyColumn); - _isValidationRunning = false; - } - - async Task ImportDataAsync(bool withoutDuplicates) - { - _isImportRunning = true; - var result = await _dataContext.ImportDataAsync(withoutDuplicates); - _importConfirmationMessage = result.Message; - _isImportRunning = false; - _isConfirmationModalDialogVisible = true; - } - - async Task ClearFormAsync() - { - _isConfirmationModalDialogVisible = false; - _step2Enabled = false; - _step3Enabled = false; - _step4Enabled = false; - await FileReaderService.CreateReference(_inputElement).ClearValue(); - _dataContext = new ImportDataViewModel(DbContextOptions); - LoadData(); - _forceShowStep1 = true; - StateHasChanged(); - } - - void ImportProfile_SelectionChanged() - { - _step3Enabled = false; - _step4Enabled = false; - if (_dataContext.SelectedImportProfile != null && - _dataContext.SelectedImportProfile.ImportProfileId != PlaceholderItemId) LoadProfile(); - } - - void TargetAccount_SelectionChanged() - { - _dataContext.SelectedImportProfile.AccountId = _dataContext.SelectedAccount?.AccountId ?? Guid.Empty; - } - - void AdditionalSettingCreditValue_SelectionChanged(ChangeEventArgs e) - { - var value = Convert.ToInt32(e.Value); - _dataContext.SelectedImportProfile.AdditionalSettingCreditValue = value; - } - - void HandleResult(ViewModelOperationResult result, string successMessage = "") - { - if (!result.IsSuccessful) - { - _errorModalDialogMessage = result.Message; - _isErrorModalDialogVisible = true; - return; - } - if (string.IsNullOrEmpty(successMessage)) return; - - _infoDialogMessage = successMessage; - _isInfoDialogVisible = true; - } -} diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor.cs b/OpenBudgeteer.Blazor/Pages/Import.razor.cs new file mode 100644 index 0000000..9aeb30f --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Import.razor.cs @@ -0,0 +1,297 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.JSInterop; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.ViewModels.EntityViewModels; +using OpenBudgeteer.Core.ViewModels.PageViewModels; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Import : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + + private ImportPageViewModel _dataContext = null!; + + private ElementReference _inputElement; + private ElementReference _step1AccordionButtonElement; + private ElementReference _step4AccordionButtonElement; + + //private const string DUMMY_COLUMN = "---Select Column---"; + + //private ImportProfileViewModel _dummyImportProfile; + + private bool _step2Enabled; + private bool _step3Enabled; + private bool _step4Enabled; + private bool _forceShowStep1; + private bool _forceShowStep4; + + private enum MappingColumn + { + TransactionDate, Payee, Memo, Amount, Credit, CreditColumnIdentifier + } + + private bool _isValidationRunning; + private bool _isImportRunning; + + private string _validationErrorMessage = string.Empty; + + private bool _isConfirmationModalDialogVisible; + private string _importConfirmationMessage = string.Empty; + + private bool _isInfoDialogVisible; + private bool _isInfoDialogInteractionEnabled; + private string _infoDialogMessage = string.Empty; + + private bool _isDeleteConfirmationDialogVisible; + + private bool _isErrorModalDialogVisible; + private string _errorModalDialogMessage = string.Empty; + + protected override void OnInitialized() + { + _dataContext = new ImportPageViewModel(ServiceManager); + + LoadData(); + LoadFromQueryParams(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (_forceShowStep1) + { + _forceShowStep1 = false; + await JSRuntime.InvokeVoidAsync("ImportPage.triggerClick", _step1AccordionButtonElement); + } + if (_forceShowStep4) + { + _forceShowStep4 = false; + await JSRuntime.InvokeVoidAsync("ImportPage.triggerClick", _step4AccordionButtonElement); + } + } + + private void LoadData() + { + HandleResult(_dataContext.LoadData()); + + _step2Enabled = false; + _step3Enabled = false; + _step4Enabled = false; + } + + private async void LoadFromQueryParams() + { + var uri = NavManager.ToAbsoluteUri(NavManager.Uri); + var query = QueryHelpers.ParseQuery(uri.Query); + + if (query.TryGetValue("csv", out var csv64)) + { + HandleResult(await LoadCsvFromBase64StringAsync(csv64!)); + } + + if (_step2Enabled && query.TryGetValue("profile", out var profileName)) + { + var profile = _dataContext.AvailableImportProfiles.First(i => i.ProfileName == profileName); + _dataContext.SelectedImportProfile = profile; + SelectedImportProfile_SelectionChanged(profile.ImportProfileId.ToString()); // Dirty solution, to be replaced by API in future + } + + if (_step4Enabled) + { + await ValidateDataAsync(); + _forceShowStep4 = true; + StateHasChanged(); + } + } + + private async Task LoadCsvFromBase64StringAsync(string csv64) + { + try + { + var csv = Encoding.UTF8.GetString(Convert.FromBase64String(csv64)); + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + await writer.WriteAsync(csv); + await writer.FlushAsync(); + stream.Position = 0; + var res = await _dataContext.HandleOpenFileAsync(stream); + if (res.IsSuccessful) + { + _step2Enabled = true; + } + return res; + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Failed to load CSV: {e.Message}"); + } + } + + private async Task ReadFileAsync() + { + LoadData(); + + _infoDialogMessage = "Uploading and processing file..."; + _isInfoDialogInteractionEnabled = false; + _isInfoDialogVisible = true; + + var file = (await FileReaderService.CreateReference(_inputElement).EnumerateFilesAsync()).FirstOrDefault(); + if (file == null) return; + HandleResult(await _dataContext.HandleOpenFileAsync(await file.OpenReadAsync())); + + _isInfoDialogVisible = false; + _step2Enabled = true; + } + + private void LoadProfile() + { + _dataContext.ResetLoadFigures(); + _step3Enabled = _dataContext.SelectedImportProfile.ImportProfileId != Guid.Empty; + CheckColumnMapping(); + StateHasChanged(); + } + + private void DeleteProfile() + { + _isDeleteConfirmationDialogVisible = false; + HandleResult(_dataContext.DeleteProfile()); + LoadData(); + _step2Enabled = true; + } + + private void LoadHeaders() + { + var result = _dataContext.LoadHeaders(); + if (result.IsSuccessful) + { + _step3Enabled = true; + } + else + { + HandleResult(result); + } + } + + private void CheckColumnMapping() + { + _step4Enabled = false; + if (string.IsNullOrEmpty(_dataContext.SelectedImportProfile.TransactionDateColumnName) || + _dataContext.SelectedImportProfile.TransactionDateColumnName == ImportPageViewModel.DummyColumn) return; + // Make Payee optional + //if (string.IsNullOrEmpty(_dataContext.PayeeColumn) || _dataContext.PayeeColumn == PLACEHOLDER_ITEM_VALUE) return; + if (string.IsNullOrEmpty(_dataContext.SelectedImportProfile.MemoColumnName) || + _dataContext.SelectedImportProfile.MemoColumnName == ImportPageViewModel.DummyColumn) return; + if (string.IsNullOrEmpty(_dataContext.SelectedImportProfile.AmountColumnName) || + _dataContext.SelectedImportProfile.AmountColumnName == ImportPageViewModel.DummyColumn) return; + _step4Enabled = true; + } + + private async Task ValidateDataAsync() + { + _isValidationRunning = true; + _validationErrorMessage = (await _dataContext.ValidateDataAsync()).Message; + _isValidationRunning = false; + } + + private async Task ImportDataAsync(bool withoutDuplicates) + { + _isImportRunning = true; + var result = await _dataContext.ImportDataAsync(withoutDuplicates); + _importConfirmationMessage = result.Message; + _isImportRunning = false; + _isConfirmationModalDialogVisible = true; + } + + private async Task ClearFormAsync() + { + _isConfirmationModalDialogVisible = false; + _step2Enabled = false; + _step3Enabled = false; + _step4Enabled = false; + await FileReaderService.CreateReference(_inputElement).ClearValue(); + _dataContext = new ImportPageViewModel(ServiceManager); + LoadData(); + _forceShowStep1 = true; + StateHasChanged(); + } + + private void SelectedImportProfile_SelectionChanged(string? value) + { + if (string.IsNullOrEmpty(value)) return; + var selection = _dataContext.AvailableImportProfiles + .First(i => i.ImportProfileId == Guid.Parse(value)); + // This copy prevents on-the-fly updates e.g. on Profile Name for AvailableImportProfiles + _dataContext.SelectedImportProfile = ImportProfileViewModel.CreateAsCopy(selection); + _step3Enabled = false; + _step4Enabled = false; + if (_dataContext.SelectedImportProfile.ImportProfileId != Guid.Empty) LoadProfile(); + } + + private void TargetAccount_SelectionChanged(string? value) + { + if (string.IsNullOrEmpty(value)) return; + _dataContext.SelectedImportProfile.Account = + _dataContext.AvailableAccounts.First(i => i.AccountId == Guid.Parse(value)); + } + + private void ColumnMapping_SelectionChanged(string? value, MappingColumn mappingColumn) + { + if (string.IsNullOrEmpty(value)) return; + var newValue = value != ImportPageViewModel.DummyColumn ? value : string.Empty; + switch (mappingColumn) + { + case MappingColumn.TransactionDate: + _dataContext.SelectedImportProfile.TransactionDateColumnName = newValue; + CheckColumnMapping(); + break; + case MappingColumn.Payee: + _dataContext.SelectedImportProfile.PayeeColumnName = newValue; + break; + case MappingColumn.Memo: + _dataContext.SelectedImportProfile.MemoColumnName = newValue; + CheckColumnMapping(); + break; + case MappingColumn.Amount: + _dataContext.SelectedImportProfile.AmountColumnName = newValue; + CheckColumnMapping(); + break; + case MappingColumn.Credit: + _dataContext.SelectedImportProfile.CreditColumnName = newValue; + break; + case MappingColumn.CreditColumnIdentifier: + _dataContext.SelectedImportProfile.CreditColumnIdentifierColumnName = newValue; + break; + default: + throw new ArgumentOutOfRangeException(nameof(mappingColumn), mappingColumn, null); + } + } + + private void AdditionalSettingCreditValue_SelectionChanged(ChangeEventArgs e) + { + var value = Convert.ToInt32(e.Value); + _dataContext.SelectedImportProfile.AdditionalSettingCreditValue = (ImportProfileViewModel.AdditionalSettingsForCreditValues)value; + } + + private void HandleResult(ViewModelOperationResult result, string successMessage = "") + { + if (!result.IsSuccessful) + { + _errorModalDialogMessage = result.Message; + _isErrorModalDialogVisible = true; + return; + } + if (string.IsNullOrEmpty(successMessage)) return; + + _infoDialogMessage = successMessage; + _isInfoDialogInteractionEnabled = true; + _isInfoDialogVisible = true; + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Index.razor b/OpenBudgeteer.Blazor/Pages/Index.razor index 542e72a..a8981f2 100644 --- a/OpenBudgeteer.Blazor/Pages/Index.razor +++ b/OpenBudgeteer.Blazor/Pages/Index.razor @@ -16,13 +16,13 @@ -
-
-
-
- +
+
+
+
+ ...
-
+
OpenBudgeteer Repository

Access source code hosted on Github

@@ -30,12 +30,12 @@
-
-
-
- +
+
+
+ ...
-
+
Create Issue

Found a bug or missing a feature? Create an issue on Github

@@ -43,12 +43,12 @@
-
-
-
- +
+
+
+ ...
-
+
Display Change Log

See latest updates and changes.

@@ -70,32 +70,3 @@
Unable to load News
} - -@code { - private string _newsSource = "https://raw.githubusercontent.com/TheAxelander/OpenBudgeteer-News/main/README.md"; - private MarkupString _convertedHtml; - private bool _showErrorMessage; - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - - await LoadNewsAsync(); - } - - async Task LoadNewsAsync() - { - try - { - _showErrorMessage = false; - var httpResponse = await new HttpClient().GetAsync(_newsSource); - httpResponse.EnsureSuccessStatusCode(); - _convertedHtml = new MarkupString(await httpResponse.Content.ReadAsStringAsync()); - } - catch - { - _convertedHtml = new MarkupString(); - _showErrorMessage = true; - } - } -} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Index.razor.cs b/OpenBudgeteer.Blazor/Pages/Index.razor.cs new file mode 100644 index 0000000..4c93722 --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Index.razor.cs @@ -0,0 +1,35 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Index : ComponentBase +{ + private const string NEWS_SOURCE = "https://raw.githubusercontent.com/TheAxelander/OpenBudgeteer-News/main/README.md"; + private MarkupString _convertedHtml; + private bool _showErrorMessage; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + await LoadNewsAsync(); + } + + private async Task LoadNewsAsync() + { + try + { + _showErrorMessage = false; + var httpResponse = await new HttpClient().GetAsync(NEWS_SOURCE); + httpResponse.EnsureSuccessStatusCode(); + _convertedHtml = new MarkupString(await httpResponse.Content.ReadAsStringAsync()); + } + catch + { + _convertedHtml = new MarkupString(); + _showErrorMessage = true; + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor b/OpenBudgeteer.Blazor/Pages/Report.razor index b142253..a721882 100644 --- a/OpenBudgeteer.Blazor/Pages/Report.razor +++ b/OpenBudgeteer.Blazor/Pages/Report.razor @@ -1,69 +1,57 @@ @page "/report" -@using ChartJs.Blazor.ChartJS.BarChart @using ChartJs.Blazor.Charts -@using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Blazor.ViewModels -@using OpenBudgeteer.Data -@inject DbContextOptions DbContextOptions
-
-

Month Balances

- +
+
+

Month Balances

+ +
-
-

Bank Balances

- +
+
+

Bank Balances

+ +
-
-

Income & Expenses per Month

- +
+
+

Income & Expenses per Month

+ +
-
-

Income & Expenses per Year

- +
+
+

Income & Expenses per Year

+ +
-
-

Bucket Monthly Expenses

- @foreach (var config in _monthBucketExpensesConfigsLeft) - { -
@config.Item1
-
- -
- } +
+
+

Bucket Monthly Expenses

+ @foreach (var config in _monthBucketExpensesConfigsLeft) + { +
@config.Item1
+
+ +
+ } +
-
-

Bucket Monthly Expenses

- @foreach (var config in _monthBucketExpensesConfigsRight) - { -
@config.Item1
-
- -
- } +
+
+

Bucket Monthly Expenses

+ @foreach (var config in _monthBucketExpensesConfigsRight) + { +
@config.Item1
+
+ +
+ } +
- -@code { - BlazorReportViewModel _dataContext; - List> _monthBucketExpensesConfigsLeft; - List> _monthBucketExpensesConfigsRight; - - protected override async Task OnInitializedAsync() - { - _monthBucketExpensesConfigsLeft = new List>(); - _monthBucketExpensesConfigsRight = new List>(); - - _dataContext = new BlazorReportViewModel(DbContextOptions); - await _dataContext.LoadDataAsync(); - - var halfIndex = _dataContext.MonthBucketExpensesConfigs.Count / 2; - _monthBucketExpensesConfigsLeft.AddRange(_dataContext.MonthBucketExpensesConfigs.ToList().GetRange(0,halfIndex)); - _monthBucketExpensesConfigsRight.AddRange(_dataContext.MonthBucketExpensesConfigs.ToList().GetRange(halfIndex,_dataContext.MonthBucketExpensesConfigs.Count - halfIndex)); - } -} diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor.cs b/OpenBudgeteer.Blazor/Pages/Report.razor.cs new file mode 100644 index 0000000..42049f0 --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Report.razor.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ChartJs.Blazor.ChartJS.BarChart; +using Microsoft.AspNetCore.Components; +using OpenBudgeteer.Blazor.ViewModels; +using OpenBudgeteer.Core.Data.Contracts.Services; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Report : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + + private BlazorReportPageViewModel _dataContext = null!; + private List> _monthBucketExpensesConfigsLeft = null!; + private List> _monthBucketExpensesConfigsRight = null!; + + protected override async Task OnInitializedAsync() + { + _monthBucketExpensesConfigsLeft = new List>(); + _monthBucketExpensesConfigsRight = new List>(); + + _dataContext = new BlazorReportPageViewModel(ServiceManager); + await _dataContext.LoadDataAsync(); + + var halfIndex = _dataContext.MonthBucketExpensesConfigs.Count / 2; + _monthBucketExpensesConfigsLeft.AddRange(_dataContext.MonthBucketExpensesConfigs.ToList().GetRange(0,halfIndex)); + _monthBucketExpensesConfigsRight.AddRange(_dataContext.MonthBucketExpensesConfigs.ToList().GetRange(halfIndex,_dataContext.MonthBucketExpensesConfigs.Count - halfIndex)); + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Rules.razor b/OpenBudgeteer.Blazor/Pages/Rules.razor index f2c02ed..4d09cbd 100644 --- a/OpenBudgeteer.Blazor/Pages/Rules.razor +++ b/OpenBudgeteer.Blazor/Pages/Rules.razor @@ -1,50 +1,44 @@ @page "/rules" -@using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.ViewModels.ItemViewModels @using System.Drawing -@using OpenBudgeteer.Contracts.Models -@using OpenBudgeteer.Core.Common -@using OpenBudgeteer.Data -@inject DbContextOptions DbContextOptions +@using OpenBudgeteer.Core.Common.Extensions +@using OpenBudgeteer.Core.ViewModels.EntityViewModels -
-
- @if (_massEditEnabled) - { - - - } - else - { - - - } -
+
+ @if (_massEditEnabled) + { + + + } + else + { + + + }
@if (_newMappingRuleSetIsEnabled) { - +
- - - - + + + + + - - - + + - - @@ -94,10 +88,11 @@
PriorityRule NameTarget BucketMapping RulesPriorityRule NameTarget BucketMapping Rules
- + + @foreach (var mappingRule in _dataContext.NewRuleSet.MappingRules) @@ -52,26 +46,26 @@
- + @@ -82,7 +76,7 @@
+
- - - - + + + + + @@ -106,14 +101,14 @@ @if (ruleSet.InModification) { - - - + + - - @@ -159,11 +154,11 @@ } else { - - - - - + + + + - } @@ -194,7 +186,7 @@ @@ -203,104 +195,3 @@ Message="@_errorModalDialogMessage" IsDialogVisible="@_isErrorModalDialogVisible" OnClickCallback="@(() => _isErrorModalDialogVisible = false)"/> - -@code { - RulesViewModel _dataContext; - bool _massEditEnabled; - bool _newMappingRuleSetIsEnabled; - - BucketViewModel _bucketSelectDialogDataContext; - bool _isBucketSelectDialogVisible; - bool _isBucketSelectDialogLoading; - RuleSetViewModelItem _ruleSetViewModelItemToBeUpdated; - - bool _isDeleteRuleSetModalDialogVisible; - RuleSetViewModelItem _ruleSetToBeDeleted; - - bool _isErrorModalDialogVisible; - string _errorModalDialogMessage; - - protected override async Task OnInitializedAsync() - { - _dataContext = new RulesViewModel(DbContextOptions); - - await HandleResult(await _dataContext.LoadDataAsync()); - } - - void CancelNewBucketRule() - { - _newMappingRuleSetIsEnabled = false; - _dataContext.ResetNewRuleSet(); - } - - void EditAllRules() - { - _massEditEnabled = true; - _dataContext.EditAllRules(); - } - - async void SaveAllRules() - { - _massEditEnabled = false; - await HandleResult(_dataContext.SaveAllRules()); - } - - async void CancelAllRules() - { - _massEditEnabled = false; - await HandleResult(await _dataContext.LoadDataAsync()); - StateHasChanged(); - } - - async void HandleUpdateSelectedBucketRequest(RuleSetViewModelItem ruleSetViewModelItem) - { - _isBucketSelectDialogVisible = true; - _isBucketSelectDialogLoading = true; - - _ruleSetViewModelItemToBeUpdated = ruleSetViewModelItem; - _bucketSelectDialogDataContext = new BucketViewModel(DbContextOptions, new YearMonthSelectorViewModel()); - await _bucketSelectDialogDataContext.LoadDataAsync(false, true); - - _isBucketSelectDialogLoading = false; - StateHasChanged(); - } - - void UpdateSelectedBucket(Contracts.Models.Bucket selectedBucket) - { - _ruleSetViewModelItemToBeUpdated.TargetBucket = selectedBucket; - _ruleSetViewModelItemToBeUpdated.RuleSet.TargetBucketId = selectedBucket.BucketId; - _isBucketSelectDialogVisible = false; - } - - void HandleRuleSetDeletionRequest(RuleSetViewModelItem ruleSet) - { - _ruleSetToBeDeleted = ruleSet; - _isDeleteRuleSetModalDialogVisible = true; - } - - void CancelDeleteRule() - { - _isDeleteRuleSetModalDialogVisible = false; - _ruleSetToBeDeleted = null; - } - - async void DeleteRule() - { - _isDeleteRuleSetModalDialogVisible = false; - await HandleResult(_dataContext.DeleteRuleSetItem(_ruleSetToBeDeleted)); - } - - async Task HandleResult(ViewModelOperationResult result) - { - if (!result.IsSuccessful) - { - _errorModalDialogMessage = result.Message; - _isErrorModalDialogVisible = true; - } - if (result.ViewModelReloadRequired) - { - await _dataContext.LoadDataAsync(); - StateHasChanged(); - } - } -} diff --git a/OpenBudgeteer.Blazor/Pages/Rules.razor.cs b/OpenBudgeteer.Blazor/Pages/Rules.razor.cs new file mode 100644 index 0000000..d7baff8 --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Rules.razor.cs @@ -0,0 +1,135 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Extensions; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.ViewModels.EntityViewModels; +using OpenBudgeteer.Core.ViewModels.Helper; +using OpenBudgeteer.Core.ViewModels.PageViewModels; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Rules : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + + private RulesPageViewModel _dataContext = null!; + private bool _massEditEnabled; + private bool _newMappingRuleSetIsEnabled; + + private BucketListingViewModel? _bucketSelectDialogDataContext; + private bool _isBucketSelectDialogVisible; + private bool _isBucketSelectDialogLoading; + private RuleSetViewModel? _ruleSetViewModelToBeUpdated; + + private bool _isDeleteRuleSetDialogVisible; + private RuleSetViewModel? _ruleSetToBeDeleted; + + private bool _isErrorModalDialogVisible; + private string _errorModalDialogMessage = string.Empty; + + protected override async Task OnInitializedAsync() + { + _dataContext = new RulesPageViewModel(ServiceManager); + + await HandleResult(await _dataContext.LoadDataAsync()); + } + + private void StartCreateMappingRuleSet() + { + _newMappingRuleSetIsEnabled = true; + } + + private void CancelNewBucketRule() + { + _newMappingRuleSetIsEnabled = false; + _dataContext.ResetNewRuleSet(); + } + + private void EditAllRules() + { + _massEditEnabled = true; + _dataContext.EditAllRules(); + } + + private async void SaveAllRules() + { + _massEditEnabled = false; + await HandleResult(_dataContext.SaveAllRules()); + } + + private async void CancelAllRules() + { + _massEditEnabled = false; + await HandleResult(await _dataContext.LoadDataAsync()); + StateHasChanged(); + } + + private async void HandleShowSelectBucketDialog(RuleSetViewModel ruleSetViewModel) + { + _isBucketSelectDialogVisible = true; + _isBucketSelectDialogLoading = true; + + _ruleSetViewModelToBeUpdated = ruleSetViewModel; + _bucketSelectDialogDataContext = new BucketListingViewModel(ServiceManager, null); + await _bucketSelectDialogDataContext.LoadDataAsync(false, true); + + _isBucketSelectDialogLoading = false; + StateHasChanged(); + } + + private void UpdateSelectedBucket(BucketViewModel selectedBucket) + { + _ruleSetViewModelToBeUpdated!.UpdateSelectedBucket(selectedBucket); + _isBucketSelectDialogVisible = false; + } + + private void ComparisionField_SelectionChanged(string? value, MappingRuleViewModel mappingRule) + { + if (string.IsNullOrEmpty(value)) return; + mappingRule.ComparisonField = Enum.TryParse(typeof(MappingRuleComparisonField), value, out var result) + ? (MappingRuleComparisonField)result + : MappingRuleComparisonField.Account; + } + + private void ComparisionType_SelectionChanged(string? value, MappingRuleViewModel mappingRule) + { + if (string.IsNullOrEmpty(value)) return; + mappingRule.ComparisonType = Enum.TryParse(typeof(MappingRuleComparisonType), value, out var result) + ? (MappingRuleComparisonType)result + : MappingRuleComparisonType.Equal; + } + + private void HandleShowDeleteRuleSetDialog(RuleSetViewModel ruleSet) + { + _ruleSetToBeDeleted = ruleSet; + _isDeleteRuleSetDialogVisible = true; + } + + private void CancelDeleteRule() + { + _isDeleteRuleSetDialogVisible = false; + _ruleSetToBeDeleted = null; + } + + private async void DeleteRule() + { + _isDeleteRuleSetDialogVisible = false; + await HandleResult(_dataContext.DeleteRuleSetItem(_ruleSetToBeDeleted!)); + } + + private async Task HandleResult(ViewModelOperationResult result) + { + if (!result.IsSuccessful) + { + _errorModalDialogMessage = result.Message; + _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + await _dataContext.LoadDataAsync(); + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index 62f10ba..4b21f5c 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -1,49 +1,38 @@ @page "/transaction" - -@using OpenBudgeteer.Core.ViewModels -@using Microsoft.EntityFrameworkCore @using System.Drawing -@using OpenBudgeteer.Core.Common +@using System.Globalization @using OpenBudgeteer.Core.Common.Extensions -@using OpenBudgeteer.Core.ViewModels.ItemViewModels -@using OpenBudgeteer.Data -@inject DbContextOptions DbContextOptions -@inject YearMonthSelectorViewModel YearMonthDataContext -
-
- @if (_massEditEnabled) - { - - - } - else - { - - - -
- - + + } + else + { + + + +
+ + - -
+ + +
+ } + - @foreach (var filter in Enum.GetValues()) - { - - } - -
- + +
@if (_newTransactionEnabled) @@ -51,42 +40,42 @@
PriorityRule NameTarget BucketMapping RulesPriorityRule NameTarget BucketMapping Rules
- + + @foreach (var mappingRule in ruleSet.MappingRules) @@ -121,26 +116,26 @@
- + @@ -151,7 +146,7 @@
+
@ruleSet.RuleSet.Priority@ruleSet.RuleSet.Name
@ruleSet.TargetBucket.Name
+
@ruleSet.Priority@ruleSet.Name
@ruleSet.TargetBucketName
    @foreach (var mappingRule in ruleSet.MappingRules) { @@ -171,12 +166,9 @@ }
- @if (ruleSet.IsHovered) - { - - - } + + +
- - - - - - - + + + + + + - - - - - - + + +
DateAccountPayeeMemoAmountBuckets + DateAccountPayeeMemoAmountBuckets
- + + + + @foreach (var bucket in _dataContext.NewTransaction.Buckets) { @@ -104,7 +93,7 @@ } -
- + @@ -116,14 +105,14 @@ - - - - - - - - + + + + + + + @@ -132,31 +121,31 @@ @if (transaction.InModification) { - - - - - - + + + - - - - + + + + @foreach (var transaction in DataContext.BucketMovementsData.Transactions) { - - - - - + + + + + } @@ -58,19 +58,25 @@
-
-

Month Balances

- +
+
+

Month Balances

+ +
-
-

Input & Output

- +
+
+

Input & Output

+ +
-
-

Balance Progression

- +
+
+

Balance Progression

+ +
@@ -88,10 +94,10 @@ @code { [Parameter] - public string Title { get; set; } + public string Title { get; set; } = string.Empty; [Parameter] - public BlazorBucketStatisticsViewModel DataContext { get; set; } + public BlazorBucketStatisticsViewModel DataContext { get; set; } = null!; [Parameter] public bool IsDialogVisible { get; set; } @@ -102,8 +108,9 @@ [Parameter] public EventCallback OnClickCallback { get; set; } - private async void HideBucketMovementCheckboxClicked(ChangeEventArgs eventArgs) + private async void HideBucketMovementCheckboxClicked(ChangeEventArgs? eventArgs) { + if (eventArgs?.Value == null) return; await DataContext.LoadBucketMovementsDataAsync((bool)eventArgs.Value); StateHasChanged(); } diff --git a/OpenBudgeteer.Blazor/Shared/BucketSelectDialog.razor b/OpenBudgeteer.Blazor/Shared/BucketSelectDialog.razor index f9a56cc..69d1519 100644 --- a/OpenBudgeteer.Blazor/Shared/BucketSelectDialog.razor +++ b/OpenBudgeteer.Blazor/Shared/BucketSelectDialog.razor @@ -1,9 +1,10 @@ @using OpenBudgeteer.Core.ViewModels @using System.Drawing -@using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Contracts.Models -@using OpenBudgeteer.Data -@inject DbContextOptions DbContextOptions +@using System.Globalization +@using OpenBudgeteer.Core.Data +@using OpenBudgeteer.Core.Data.Entities.Models +@using OpenBudgeteer.Core.ViewModels.EntityViewModels +@using OpenBudgeteer.Core.ViewModels.Helper @inject YearMonthSelectorViewModel YearMonthDataContext @if (IsDialogVisible) @@ -27,9 +28,9 @@
- - - + + + @@ -44,13 +45,13 @@
- @bucketGroup.BucketGroup.Name + @bucketGroup.Name
- - - - + + + + @@ -62,21 +63,21 @@ { - - - - + + +
DateAccountPayeeMemoAmountBuckets +
DateAccountPayeeMemoAmountBuckets
- + + + + @foreach (var bucket in transaction.Buckets) { @@ -174,7 +163,7 @@ } - - - - - - - + + + + + + - } @@ -211,24 +197,8 @@
- + @@ -183,27 +172,24 @@ } else { -
@transaction.Transaction.TransactionDate.ToShortDateString()@transaction.SelectedAccount.Name@transaction.Transaction.Payee@transaction.Transaction.Memo@transaction.Transaction.Amount +
@transaction.TransactionDate.ToShortDateString()@transaction.SelectedAccount.Name@transaction.Payee@transaction.Memo@transaction.Amount.ToString("C", CultureInfo.CurrentCulture) @foreach (var bucket in transaction.Buckets) { -
-
@bucket.SelectedBucket.Name
-
@bucket.Amount
+
+
@bucket.SelectedBucketName
+
@bucket.Amount.ToString("C", CultureInfo.CurrentCulture)
}
- @if (transaction.IsHovered) - { - - - } + + +
-@if (_isProposeBucketsInfoDialogVisible) -{ - - -} - @@ -242,158 +212,18 @@ + + - -@code { - TransactionViewModel _dataContext; - bool _newTransactionEnabled; - bool _massEditEnabled; - - BucketViewModel _bucketSelectDialogDataContext; - bool _isBucketSelectDialogVisible; - bool _isBucketSelectDialogLoading; - PartialBucketViewModelItem _partialBucketViewModelItemToBeUpdated; - - bool _isRecurringTransactionModalDialogVisible; - RecurringTransactionViewModel _recurringTransactionViewModel; - - bool _isDeleteTransactionModalDialogVisible; - TransactionViewModelItem _transactionToBeDeleted; - - bool _isErrorModalDialogVisible; - string _errorModalDialogMessage; - - bool _isProposeBucketsInfoDialogVisible; - - protected override async Task OnInitializedAsync() - { - _dataContext = new TransactionViewModel(DbContextOptions, YearMonthDataContext); - - await HandleResult(await _dataContext.LoadDataAsync()); - - YearMonthDataContext.SelectedYearMonthChanged += async (sender, args) => - { - await HandleResult(await _dataContext.LoadDataAsync()); - StateHasChanged(); - }; - } - - void CancelNewTransaction() - { - _newTransactionEnabled = false; - _dataContext.ResetNewTransaction(); - } - - void EditAllTransaction() - { - _massEditEnabled = true; - _dataContext.EditAllTransaction(); - } - - async Task ProposeBucketsAsync() - { - _isProposeBucketsInfoDialogVisible = true; - StateHasChanged(); - await _dataContext.ProposeBuckets(); - if (_dataContext.Transactions.Any(i => i.InModification)) _massEditEnabled = true; - _isProposeBucketsInfoDialogVisible = false; - } - - void SplitTransaction(TransactionViewModelItem transaction) => - transaction.AddBucketItem(transaction.Transaction.Amount - transaction.Buckets.Sum(b => b.Amount)); - - async void SaveAllTransaction() - { - _massEditEnabled = false; - await HandleResult(_dataContext.SaveAllTransaction()); - } - - async void CancelAllTransaction() - { - _massEditEnabled = false; - await HandleResult(await _dataContext.CancelAllTransactionAsync()); - StateHasChanged(); - } - - async void SaveTransaction(TransactionViewModelItem transaction) - { - await HandleResult(transaction.UpdateItem()); - } - - void Filter_SelectionChanged(ChangeEventArgs e) - { - _dataContext.CurrentFilter = Enum.Parse( - e.Value as string ?? TransactionViewModelFilter.NoFilter.ToString()); - } - - void HandleTransactionDeletionRequest(TransactionViewModelItem transaction) - { - _transactionToBeDeleted = transaction; - _isDeleteTransactionModalDialogVisible = true; - } - - void CancelDeleteTransaction() - { - _isDeleteTransactionModalDialogVisible = false; - _transactionToBeDeleted = null; - } - - async void DeleteTransaction() - { - _isDeleteTransactionModalDialogVisible = false; - await HandleResult(_transactionToBeDeleted.DeleteItem()); - } - - async void AddRecurringTransactions() - { - await HandleResult(await _dataContext.AddRecurringTransactionsAsync()); - } - - async Task DisplayRecurringTransactions() - { - _recurringTransactionViewModel = new RecurringTransactionViewModel(DbContextOptions); - await _recurringTransactionViewModel.LoadDataAsync(); - _isRecurringTransactionModalDialogVisible = true; - } - - async void HandleUpdateSelectedBucketRequest(PartialBucketViewModelItem partialBucketViewModelItem) - { - _isBucketSelectDialogVisible = true; - _isBucketSelectDialogLoading = true; - - _partialBucketViewModelItemToBeUpdated = partialBucketViewModelItem; - _bucketSelectDialogDataContext = new BucketViewModel(DbContextOptions, YearMonthDataContext); - await _bucketSelectDialogDataContext.LoadDataAsync(true, true); - - _isBucketSelectDialogLoading = false; - StateHasChanged(); - } - - void UpdateSelectedBucket(Contracts.Models.Bucket selectedBucket) - { - _partialBucketViewModelItemToBeUpdated.SelectedBucket = selectedBucket; - _isBucketSelectDialogVisible = false; - } - - async Task HandleResult(ViewModelOperationResult result) - { - if (!result.IsSuccessful) - { - _errorModalDialogMessage = result.Message; - _isErrorModalDialogVisible = true; - } - if (result.ViewModelReloadRequired) - { - await _dataContext.LoadDataAsync(); - StateHasChanged(); - } - } -} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor.cs b/OpenBudgeteer.Blazor/Pages/Transaction.razor.cs new file mode 100644 index 0000000..428f5b9 --- /dev/null +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor.cs @@ -0,0 +1,182 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Extensions; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.ViewModels.EntityViewModels; +using OpenBudgeteer.Core.ViewModels.Helper; +using OpenBudgeteer.Core.ViewModels.PageViewModels; + +namespace OpenBudgeteer.Blazor.Pages; + +public partial class Transaction : ComponentBase +{ + [Inject] private IServiceManager ServiceManager { get; set; } = null!; + [Inject] private YearMonthSelectorViewModel YearMonthDataContext { get; set; } = null!; + + private TransactionPageViewModel _dataContext = null!; + private bool _newTransactionEnabled; + private bool _massEditEnabled; + + private BucketListingViewModel? _bucketSelectDialogDataContext; + private bool _isBucketSelectDialogVisible; + private bool _isBucketSelectDialogLoading; + private PartialBucketViewModel? _partialBucketViewModelToBeUpdated; + + private bool _isRecurringTransactionModalDialogVisible; + private RecurringTransactionHandlerViewModel? _recurringTransactionHandlerViewModel; + + private bool _isDeleteTransactionDialogVisible; + private TransactionViewModel? _transactionToBeDeleted; + + private bool _isErrorModalDialogVisible; + private string _errorModalDialogMessage = string.Empty; + + private bool _isProposeBucketsInfoDialogVisible; + + protected override async Task OnInitializedAsync() + { + _dataContext = new TransactionPageViewModel(ServiceManager, YearMonthDataContext); + + await HandleResult(await _dataContext.LoadDataAsync()); + + YearMonthDataContext.SelectedYearMonthChanged += async (sender, args) => + { + await HandleResult(await _dataContext.LoadDataAsync()); + StateHasChanged(); + }; + } + + private void StartCreateNewTransaction() + { + _newTransactionEnabled = true; + } + + private void CancelNewTransaction() + { + _newTransactionEnabled = false; + _dataContext.ResetNewTransaction(); + } + + private void EditAllTransaction() + { + _massEditEnabled = true; + _dataContext.EditAllTransaction(); + } + + private void NewTransactionAccount_SelectionChanged(string? value) + { + if (string.IsNullOrEmpty(value)) return; + _dataContext.NewTransaction!.SelectedAccount = _dataContext.NewTransaction!.AvailableAccounts.First(i => i.AccountId == Guid.Parse(value)); + } + + private async Task ProposeBucketsAsync() + { + _isProposeBucketsInfoDialogVisible = true; + StateHasChanged(); + await _dataContext.ProposeBuckets(); + if (_dataContext.Transactions.Any(i => i.InModification)) _massEditEnabled = true; + _isProposeBucketsInfoDialogVisible = false; + } + + private void TransactionAccount_SelectionChanged(string? value, TransactionViewModel transactionViewModel) + { + if (string.IsNullOrEmpty(value)) return; + transactionViewModel.SelectedAccount = transactionViewModel.AvailableAccounts.First(i => i.AccountId == Guid.Parse(value)); + } + + private void SplitTransaction(TransactionViewModel transaction) => + transaction.AddBucketItem(transaction.Amount - transaction.Buckets.Sum(b => b.Amount)); + + private async void SaveAllTransaction() + { + _massEditEnabled = false; + await HandleResult(_dataContext.SaveAllTransaction()); + } + + private async void CancelAllTransaction() + { + _massEditEnabled = false; + await HandleResult(await _dataContext.CancelAllTransactionAsync()); + StateHasChanged(); + } + + private async void SaveTransaction(TransactionViewModel transaction) + { + await HandleResult(transaction.CreateOrUpdateTransaction()); + } + + private void Filter_SelectionChanged(string? value) + { + if (string.IsNullOrEmpty(value)) return; + + _dataContext.CurrentFilter = Enum.TryParse(typeof(TransactionFilter), value, out var result) + ? (TransactionFilter)result + : TransactionFilter.NoFilter; + } + + private void HandleShowDeletionTransactionDialog(TransactionViewModel transaction) + { + _transactionToBeDeleted = transaction; + _isDeleteTransactionDialogVisible = true; + } + + private void CancelDeleteTransaction() + { + _isDeleteTransactionDialogVisible = false; + _transactionToBeDeleted = null; + } + + private async void DeleteTransaction() + { + _isDeleteTransactionDialogVisible = false; + await HandleResult(_transactionToBeDeleted!.DeleteTransaction()); + } + + private async void AddRecurringTransactions() + { + await HandleResult(await _dataContext.AddRecurringTransactionsAsync()); + } + + private async Task DisplayRecurringTransactions() + { + _recurringTransactionHandlerViewModel = new RecurringTransactionHandlerViewModel(ServiceManager); + await _recurringTransactionHandlerViewModel.LoadDataAsync(); + _isRecurringTransactionModalDialogVisible = true; + } + + private async void HandleShowBucketSelectDialog(PartialBucketViewModel partialBucketViewModel) + { + _isBucketSelectDialogVisible = true; + _isBucketSelectDialogLoading = true; + + _partialBucketViewModelToBeUpdated = partialBucketViewModel; + _bucketSelectDialogDataContext = new BucketListingViewModel(ServiceManager, YearMonthDataContext); + await _bucketSelectDialogDataContext.LoadDataAsync(true, true); + + _isBucketSelectDialogLoading = false; + StateHasChanged(); + } + + private void UpdateSelectedBucket(BucketViewModel selectedBucket) + { + _partialBucketViewModelToBeUpdated!.UpdateSelectedBucket(selectedBucket); + _isBucketSelectDialogVisible = false; + } + + private async Task HandleResult(ViewModelOperationResult result) + { + if (!result.IsSuccessful) + { + _errorModalDialogMessage = result.Message; + _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + await _dataContext.LoadDataAsync(); + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Pages/_Host.cshtml b/OpenBudgeteer.Blazor/Pages/_Host.cshtml deleted file mode 100644 index b9fdb98..0000000 --- a/OpenBudgeteer.Blazor/Pages/_Host.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@page "/" -@using Microsoft.AspNetCore.Mvc.TagHelpers -@namespace OpenBudgeteer.Blazor.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@{ - Layout = "_Layout"; -} - - diff --git a/OpenBudgeteer.Blazor/Pages/_Layout.cshtml b/OpenBudgeteer.Blazor/Pages/_Layout.cshtml deleted file mode 100644 index aa7180f..0000000 --- a/OpenBudgeteer.Blazor/Pages/_Layout.cshtml +++ /dev/null @@ -1,74 +0,0 @@ -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Mvc.TagHelpers -@using OpenBudgeteer.Core.Common; -@namespace OpenBudgeteer.Blazor.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - - - - - - - OpenBudgeteer - - - - - - - - - - - - @RenderBody() - -
- - An error has occurred. This application may no longer respond until reloaded. - - - An unhandled exception has occurred. See browser dev tools for details. - - Reload - 🗙 -
- -
-
- - App Icons by Icons8 - -
-
- - - - - - - - - - - - - - - - - - diff --git a/OpenBudgeteer.Blazor/Program.cs b/OpenBudgeteer.Blazor/Program.cs index f70e391..d11d58b 100644 --- a/OpenBudgeteer.Blazor/Program.cs +++ b/OpenBudgeteer.Blazor/Program.cs @@ -1,32 +1,60 @@ using System; -using System.Linq; -using Microsoft.AspNetCore.Hosting; +using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using OpenBudgeteer.Data; +using OpenBudgeteer.Blazor; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Data; +using OpenBudgeteer.Core.Data.Contracts.Services; +using OpenBudgeteer.Core.Data.Entities; +using OpenBudgeteer.Core.Data.Services; +using OpenBudgeteer.Core.ViewModels.Helper; +using Tewr.Blazor.FileReader; -namespace OpenBudgeteer.Blazor; +const string APPSETTINGS_CULTURE = "APPSETTINGS_CULTURE"; +const string APPSETTINGS_THEME = "APPSETTINGS_THEME"; -public class Program +AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddLocalization(); +builder.Services.AddRazorPages(); +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); +builder.Services.AddFileReaderService(); +builder.Services.AddHostedService(); +builder.Services.AddDatabase(builder.Configuration); +builder.Services.AddScoped(x => new ServiceManager(x.GetRequiredService>())); +builder.Services.AddScoped(x => new YearMonthSelectorViewModel(x.GetRequiredService())); + +Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Required to read ANSI Text files + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) { - public static void Main(string[] args) - { - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); - CreateHostBuilder(args).Build().Run(); - } - - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices(service => - { - service.AddHostedService(); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - - + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); } + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRequestLocalization(builder.Configuration.GetValue(APPSETTINGS_CULTURE, "en-US") ?? "en-US"); +AppSettings.Theme = builder.Configuration.GetValue(APPSETTINGS_THEME, "Default") ?? "Default"; + +//app.UseRouting(); +app.UseAntiforgery(); +/*app.UseEndpoints(endpoints => +{ + endpoints.MapRazorComponents().AddInteractiveServerRenderMode(); +});*/ +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); + diff --git a/OpenBudgeteer.Blazor/Routes.razor b/OpenBudgeteer.Blazor/Routes.razor new file mode 100644 index 0000000..f2a6457 --- /dev/null +++ b/OpenBudgeteer.Blazor/Routes.razor @@ -0,0 +1,10 @@ + + + + + + +

Sorry, there's nothing at this address.

+
+
+
\ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Shared/BucketDetailsDialog.razor b/OpenBudgeteer.Blazor/Shared/BucketDetailsDialog.razor index 9cf2a06..dce5878 100644 --- a/OpenBudgeteer.Blazor/Shared/BucketDetailsDialog.razor +++ b/OpenBudgeteer.Blazor/Shared/BucketDetailsDialog.razor @@ -2,7 +2,7 @@ @using ChartJs.Blazor.ChartJS.BarChart @using ChartJs.Blazor.Charts @using OpenBudgeteer.Blazor.ViewModels -@using OpenBudgeteer.Contracts.Models +@using System.Globalization @if (IsDialogVisible) { @@ -36,21 +36,21 @@
DateAccountPayeeMemoAmountAccountPayeeMemoAmount
@transaction.Transaction.TransactionDate.ToShortDateString()@transaction.SelectedAccount.Name@transaction.Transaction.Payee@transaction.Transaction.Memo@transaction.Transaction.Amount@transaction.TransactionDate.ToShortDateString()@transaction.SelectedAccount.Name@transaction.Payee@transaction.Memo@transaction.Amount.ToString("C", CultureInfo.CurrentCulture)
Bucket BalanceInActivityDetailsInActivityDetails
@bucketGroup.TotalBalance.ToString("N2")@(bucketGroup.TotalIn == 0 ? string.Empty : bucketGroup.TotalIn.ToString("N2"))@(bucketGroup.TotalActivity == 0 ? string.Empty : bucketGroup.TotalActivity.ToString("N2"))@bucketGroup.TotalBalance.ToString("C", CultureInfo.CurrentCulture)@(bucketGroup.TotalIn == 0 ? string.Empty : bucketGroup.TotalIn.ToString("C", CultureInfo.CurrentCulture))@(bucketGroup.TotalActivity == 0 ? string.Empty : bucketGroup.TotalActivity.ToString("C", CultureInfo.CurrentCulture))
- @bucket.Balance.ToString("N2")@(bucket.In == 0 ? string.Empty : bucket.In.ToString("N2"))@(bucket.Activity == 0 ? string.Empty : bucket.Activity.ToString("N2")) + @bucket.Balance.ToString("C", CultureInfo.CurrentCulture)@(bucket.In == 0 ? string.Empty : bucket.In.ToString("C", CultureInfo.CurrentCulture))@(bucket.Activity == 0 ? string.Empty : bucket.Activity.ToString("C", CultureInfo.CurrentCulture)) @if (bucket.IsProgressbarVisible) {
@@ -113,8 +114,8 @@ @code { [Parameter] - public BucketViewModel DataContext { get; set; } - + public BucketListingViewModel DataContext { get; set; } = null!; + [Parameter] public bool IsDialogVisible { get; set; } @@ -122,7 +123,7 @@ public bool IsDialogLoading { get; set; } [Parameter] - public EventCallback OnBucketSelectedCallback { get; set; } + public EventCallback OnBucketSelectedCallback { get; set; } [Parameter] public EventCallback OnCancelCallback { get; set; } diff --git a/OpenBudgeteer.Blazor/Shared/BucketStatsElement.razor b/OpenBudgeteer.Blazor/Shared/BucketStatsElement.razor new file mode 100644 index 0000000..cfb3b5d --- /dev/null +++ b/OpenBudgeteer.Blazor/Shared/BucketStatsElement.razor @@ -0,0 +1,21 @@ +
+
+ +
+
+ @Title +
+ @Amount +
+
+ +@code { + [Parameter] + public string? ImageUrl { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Amount { get; set; } +} \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/Shared/DeleteConfirmationDialog.razor b/OpenBudgeteer.Blazor/Shared/DeleteConfirmationDialog.razor index 02cd4f8..701dd8c 100644 --- a/OpenBudgeteer.Blazor/Shared/DeleteConfirmationDialog.razor +++ b/OpenBudgeteer.Blazor/Shared/DeleteConfirmationDialog.razor @@ -19,11 +19,12 @@ } @code { - [Parameter] - public string Title { get; set; } [Parameter] - public string Message { get; set; } + public string Title { get; set; } = string.Empty; + + [Parameter] + public string Message { get; set; } = string.Empty; [Parameter] public bool IsDialogVisible { get; set; } diff --git a/OpenBudgeteer.Blazor/Shared/EditAccountDialog.razor b/OpenBudgeteer.Blazor/Shared/EditAccountDialog.razor index e505738..1184886 100644 --- a/OpenBudgeteer.Blazor/Shared/EditAccountDialog.razor +++ b/OpenBudgeteer.Blazor/Shared/EditAccountDialog.razor @@ -1,4 +1,4 @@ -@using OpenBudgeteer.Core.ViewModels.ItemViewModels +@using OpenBudgeteer.Core.ViewModels.EntityViewModels @if (IsDialogVisible) {