diff --git a/.aspire/settings.json b/.aspire/settings.json
new file mode 100644
index 000000000..deede96df
--- /dev/null
+++ b/.aspire/settings.json
@@ -0,0 +1,3 @@
+{
+ "appHostPath": "../src/src.AppHost/src.AppHost.csproj"
+}
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
deleted file mode 100644
index 45434e736..000000000
--- a/.devcontainer/devcontainer.json
+++ /dev/null
@@ -1,38 +0,0 @@
-// For format details, see https://aka.ms/devcontainer.json. For config options, see the
-// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
-{
- "name": "AccountGo (.NET)",
- // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
- "image": "mcr.microsoft.com/devcontainers/dotnet:0-7.0-bullseye",
- "features": {
- "ghcr.io/devcontainers/features/azure-cli:1": {},
- "ghcr.io/devcontainers/features/git:1": {},
- "ghcr.io/dhoeric/features/google-cloud-cli:1": {},
- "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {},
- "ghcr.io/devcontainers/features/docker-in-docker:1": {
- "version": "latest",
- "moby": true
- },
- "ghcr.io/devcontainers/features/node:1": {}
- }
-
- // Features to add to the dev container. More info: https://containers.dev/features.
- // "features": {},
-
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
- // "forwardPorts": [5000, 5001],
- // "portsAttributes": {
- // "5001": {
- // "protocol": "https"
- // }
- // }
-
- // Use 'postCreateCommand' to run commands after the container is created.
- // "postCreateCommand": "dotnet restore",
-
- // Configure tool-specific properties.
- // "customizations": {},
-
- // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
- // "remoteUser": "root"
-}
diff --git a/.github/workflows/build-deploy-azure.yml b/.github/workflows/build-deploy-azure.yml
deleted file mode 100644
index eb332b2cd..000000000
--- a/.github/workflows/build-deploy-azure.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: Docker Image CI
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
- workflow_dispatch:
-
-jobs:
-
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Build the Docker image
- run: docker-compose build
diff --git a/.github/workflows/gdbapi.yml b/.github/workflows/gdbapi.yml
new file mode 100644
index 000000000..30eb05c91
--- /dev/null
+++ b/.github/workflows/gdbapi.yml
@@ -0,0 +1,94 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books API to Azure Web App - gdbapi
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.x'
+
+ - name: Install dotnet-ef tool
+ run: |
+ dotnet tool install --global dotnet-ef
+ echo "++++ dotnet-ef version"
+ dotnet ef --version
+
+ - name: Install dotnet aspire workload
+ run: |
+ dotnet workload install aspire
+
+ - name: Build with dotnet
+ run: |
+ echo "++++ dotnet build"
+ dotnet build --configuration Release
+
+ - name: Run unit tests
+ run: dotnet test ./test/GoodBooks.BackendTests/GoodBooks.BackendTests.csproj --configuration Release
+
+ - name: Add migrations
+ run: |
+ echo "++++ current directory"
+ pwd
+ echo "++++ add ApplicationIdentityDbContext migration IdentityMig"
+ dotnet ef migrations add IdentityMig --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext --output-dir Data/Migrations/IdentityDb
+ echo "++++ add ApiDbContext migration ApiMig"
+ dotnet ef migrations add ApiMig --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext --output-dir Data/Migrations/ApiDb
+ echo "++++ contents of ./src/Api/Data/Migrations/IdentityDb"
+ ls ./src/Api/Data/Migrations/IdentityDb
+ echo "++++ contents of ./src/Api/Data/Migrations/ApiDb"
+ ls ./src/Api/Data/Migrations/ApiDb
+
+ - name: dotnet publish
+ run: |
+ echo "++++ contents of dotnet publish ./src/Api/Api.csproj"
+ dotnet publish ./src/Api/Api.csproj -f net9.0 -c Release -o "${{runner.temp}}/myapp"
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: ${{runner.temp}}/myapp
+
+ deploy:
+ runs-on: windows-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_A17E281C175C4E629A76134AA823BAC5 }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_258CF23452C24D9795BD94B25EF50B73 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_9375B274C69740D39F4770D5D433E8B1 }}
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'gdbapi'
+ slot-name: 'Production'
+ package: .
diff --git a/.github/workflows/gdbmvc_tar.yml b/.github/workflows/gdbmvc_tar.yml
new file mode 100644
index 000000000..4939fb8e4
--- /dev/null
+++ b/.github/workflows/gdbmvc_tar.yml
@@ -0,0 +1,99 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+name: Build and deploy Good Deed Books MVC project to Azure
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.x'
+ include-prerelease: true
+
+ - name: Install dotnet aspire workload
+ run: |
+ dotnet workload install aspire
+
+ - name: Build with dotnet
+ run: dotnet build --configuration Release
+
+ - name: Run unit tests
+ run: dotnet test ./test/GoodBooks.BackendTests/GoodBooks.BackendTests.csproj --configuration Release
+
+ - name: dotnet publish
+ run: dotnet publish ./src/AccountGoWeb/AccountGoWeb.csproj -c Release -o ${{runner.temp}}/myapp
+
+ - name: Archive production artifacts
+ run: |
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ save current directory into a variable dir ++++"
+ dir=$(pwd)
+ echo "+++++++++ what is in variable dir ++++++++++++++"
+ echo $dir
+ echo "++++++++++++++++++++++++ what's in current directory? ++++++++"
+ ls -al
+ echo "+++++ what's in the ${{runner.temp}}/myapp directory? ++++"
+ ls -al ${{runner.temp}}/myapp
+ echo "+++++ change directory to ${{runner.temp}}/myapp ++++"
+ cd ${{runner.temp}}/myapp
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ compress current directory and save in $dir/my_artifact.tar.gz ++++"
+ tar -czvf $dir/my_artifact.tar.gz .
+ echo "+++++++++++++++++++++++++ change back to $dir directory ++++"
+ cd $dir
+ echo "++++++++++++++++++++++++ what's in $dir directory? ++++++++"
+ ls -al
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: my_artifact.tar.gz
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_8B6389BB3F37413FB2483AC2574C3BCB }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_FD62C59DE5DC42C2A07DB8191A522348 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_7076EF307FDA4C11BC99A0A7A0943794 }}
+
+ - name: Extract artifacts
+ run: |
+ tar -xzvf my_artifact.tar.gz -C .
+
+ - name: Set startup command
+ run: |
+ az webapp config set --resource-group goodbooks-RG --name gdbmvc --startup-file "dotnet /home/site/wwwroot/GoodBooks.dll"
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'gdbmvc'
+ slot-name: 'Production'
+ package: .
diff --git a/.gitignore b/.gitignore
index fbb3738a8..6272404dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
+# custom 2024/04/29
+.idea
+
# User-specific files
*.suo
*.user
@@ -18,6 +21,7 @@ build/
bld/
[Bb]in/
[Oo]bj/
+node_modules/
# Roslyn cache directories
*.ide/
@@ -186,6 +190,10 @@ FakesAssemblies/
# Lib folder generated by gulpfile.js
**/src/[Ww]eb[Aa]ngular/wwwroot/[Ll]ib/*
+
+
+**/src/[Rr]eact[Ff]ront[Ee]nd/wwwroot/*
+
**/src/[Ww]eb[Aa]pp/wwwroot/app/scripts/*
**/src/[Ww]eb[Aa]pp/wwwroot/app/compiledscripts/*
**/src/[Ww]eb[Aa]pp/wwwroot/app/typescripts/compiledscripts/*
@@ -215,7 +223,8 @@ FakesAssemblies/
/src/Api/Plugins/*
/src/AccountGoWeb/Modules/*
/src/AccountGoWeb/Plugins/*
-/src/Api/Data/Migrations
+# /src/Api/Data/Migrations
.vscode
-exclude
\ No newline at end of file
+exclude
+/src/Api/appsettings.Development.json
diff --git a/accountgo.sln b/accountgo.sln
index 4eaa3f47d..29ef7bb4d 100644
--- a/accountgo.sln
+++ b/accountgo.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26228.4
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34322.80
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{0295DFAC-BF6E-46C0-A63D-FBE9AF3C04E5}"
EndProject
@@ -19,62 +19,234 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountGoWeb", "src\Account
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dto", "src\Dto\Dto.csproj", "{1E610F55-2D74-4856-818B-0D0B47601B75}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E1B45442-3F2D-491A-9D8A-0DDA50309A1A}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{EBFAFB5B-494F-48D5-A70D-AF1490B9260A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0EAC5155-A5EA-49C1-8E0C-19DD36D2C21C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Module.Tests", "test\Module.Tests\Module.Tests.csproj", "{54631590-2A41-45F4-B057-92C840ED08C1}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{E0861852-0F5B-4810-8586-A59038BC4034}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GoodBooks.BackendTests", "test\GoodBooks.BackendTests\GoodBooks.BackendTests.csproj", "{C59F300E-4BAC-4329-9A41-8F1D75A7E197}"
+EndProject
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EF0BD6F1-00D6-41E5-91AB-8B606D35D448}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorGDB", "src\BlazorGDB\BlazorGDB\BlazorGDB.csproj", "{AB5F238F-AB78-4A85-8D8D-17E211015FD3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorGDB.Client", "src\BlazorGDB\BlazorGDB.Client\BlazorGDB.Client.csproj", "{12BE663C-C0DD-4343-93DF-6B2D853B6B79}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibraryGDB", "src\LibraryGDB\LibraryGDB.csproj", "{F64790E0-86AD-4562-9AC5-F4DD3F4881BA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{64880D93-BAB4-FF83-898C-B934B68C31A9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "src.ServiceDefaults", "src\src.ServiceDefaults\src.ServiceDefaults.csproj", "{949C95E9-4261-416E-8D2A-F05E3D4640CA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "src.AppHost", "src\src.AppHost\src.AppHost.csproj", "{ADDBCE30-FE7F-4198-8A37-772EF6BF3676}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "src\Modules\SampleModule\SampleModule.csproj", "{B296277A-C822-444E-8CFA-4CC4C1C1F737}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationService", "src\MigrationService\MigrationService.csproj", "{DF084D96-707B-47C2-9493-85FA84631ACE}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleNetStandard20", "test\SampleModules\SampleNetStandard20\SampleNetStandard20.csproj", "{B0AB6EA7-7D53-4457-9482-F0613F99E3BB}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoodBooks.ServicesTests", "test\GoodBooks.ServicesTests\GoodBooks.ServicesTests.csproj", "{1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Debug|x64.Build.0 = Debug|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Debug|x86.Build.0 = Debug|Any CPU
{9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Release|x64.ActiveCfg = Release|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Release|x64.Build.0 = Release|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Release|x86.ActiveCfg = Release|Any CPU
+ {9B652491-4E9C-45E0-BE5B-EA6AF892F380}.Release|x86.Build.0 = Release|Any CPU
{C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Debug|x64.Build.0 = Debug|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Debug|x86.Build.0 = Debug|Any CPU
{C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Release|x64.ActiveCfg = Release|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Release|x64.Build.0 = Release|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Release|x86.ActiveCfg = Release|Any CPU
+ {C02DECC9-2A82-42C0-8F26-D0AE6559AC5E}.Release|x86.Build.0 = Release|Any CPU
{09096FEC-DA29-4914-B046-CD280220C52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09096FEC-DA29-4914-B046-CD280220C52A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Debug|x64.Build.0 = Debug|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Debug|x86.Build.0 = Debug|Any CPU
{09096FEC-DA29-4914-B046-CD280220C52A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09096FEC-DA29-4914-B046-CD280220C52A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Release|x64.ActiveCfg = Release|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Release|x64.Build.0 = Release|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Release|x86.ActiveCfg = Release|Any CPU
+ {09096FEC-DA29-4914-B046-CD280220C52A}.Release|x86.Build.0 = Release|Any CPU
{9CA13D2D-D6E2-4201-946C-81D1E6093404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CA13D2D-D6E2-4201-946C-81D1E6093404}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Debug|x64.Build.0 = Debug|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Debug|x86.Build.0 = Debug|Any CPU
{9CA13D2D-D6E2-4201-946C-81D1E6093404}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CA13D2D-D6E2-4201-946C-81D1E6093404}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Release|x64.ActiveCfg = Release|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Release|x64.Build.0 = Release|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Release|x86.ActiveCfg = Release|Any CPU
+ {9CA13D2D-D6E2-4201-946C-81D1E6093404}.Release|x86.Build.0 = Release|Any CPU
{1E610F55-2D74-4856-818B-0D0B47601B75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E610F55-2D74-4856-818B-0D0B47601B75}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Debug|x64.Build.0 = Debug|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Debug|x86.Build.0 = Debug|Any CPU
{1E610F55-2D74-4856-818B-0D0B47601B75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E610F55-2D74-4856-818B-0D0B47601B75}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Release|x64.ActiveCfg = Release|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Release|x64.Build.0 = Release|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Release|x86.ActiveCfg = Release|Any CPU
+ {1E610F55-2D74-4856-818B-0D0B47601B75}.Release|x86.Build.0 = Release|Any CPU
{EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Debug|x64.Build.0 = Debug|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Debug|x86.Build.0 = Debug|Any CPU
{EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Release|x64.ActiveCfg = Release|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Release|x64.Build.0 = Release|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Release|x86.ActiveCfg = Release|Any CPU
+ {EBFAFB5B-494F-48D5-A70D-AF1490B9260A}.Release|x86.Build.0 = Release|Any CPU
{54631590-2A41-45F4-B057-92C840ED08C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54631590-2A41-45F4-B057-92C840ED08C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {54631590-2A41-45F4-B057-92C840ED08C1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {54631590-2A41-45F4-B057-92C840ED08C1}.Debug|x64.Build.0 = Debug|Any CPU
+ {54631590-2A41-45F4-B057-92C840ED08C1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {54631590-2A41-45F4-B057-92C840ED08C1}.Debug|x86.Build.0 = Debug|Any CPU
{54631590-2A41-45F4-B057-92C840ED08C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54631590-2A41-45F4-B057-92C840ED08C1}.Release|Any CPU.Build.0 = Release|Any CPU
- {B296277A-C822-444E-8CFA-4CC4C1C1F737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B296277A-C822-444E-8CFA-4CC4C1C1F737}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B296277A-C822-444E-8CFA-4CC4C1C1F737}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B296277A-C822-444E-8CFA-4CC4C1C1F737}.Release|Any CPU.Build.0 = Release|Any CPU
- {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C59F300E-4BAC-4329-9A41-8F1D75A7E197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C59F300E-4BAC-4329-9A41-8F1D75A7E197}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C59F300E-4BAC-4329-9A41-8F1D75A7E197}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C59F300E-4BAC-4329-9A41-8F1D75A7E197}.Release|Any CPU.Build.0 = Release|Any CPU
{B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Debug|x64.Build.0 = Debug|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Debug|x86.Build.0 = Debug|Any CPU
{B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Release|x64.ActiveCfg = Release|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Release|x64.Build.0 = Release|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Release|x86.ActiveCfg = Release|Any CPU
+ {B0AB6EA7-7D53-4457-9482-F0613F99E3BB}.Release|x86.Build.0 = Release|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Debug|x64.Build.0 = Debug|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Debug|x86.Build.0 = Debug|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Release|x64.ActiveCfg = Release|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Release|x64.Build.0 = Release|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Release|x86.ActiveCfg = Release|Any CPU
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}.Release|x86.Build.0 = Release|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Debug|x64.Build.0 = Debug|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Debug|x86.Build.0 = Debug|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Release|x64.ActiveCfg = Release|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Release|x64.Build.0 = Release|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Release|x86.ActiveCfg = Release|Any CPU
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3}.Release|x86.Build.0 = Release|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Debug|x64.Build.0 = Debug|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Debug|x86.Build.0 = Debug|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Release|Any CPU.Build.0 = Release|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Release|x64.ActiveCfg = Release|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Release|x64.Build.0 = Release|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Release|x86.ActiveCfg = Release|Any CPU
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79}.Release|x86.Build.0 = Release|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Debug|x64.Build.0 = Debug|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Debug|x86.Build.0 = Debug|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Release|x64.ActiveCfg = Release|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Release|x64.Build.0 = Release|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Release|x86.ActiveCfg = Release|Any CPU
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA}.Release|x86.Build.0 = Release|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Debug|x64.Build.0 = Debug|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Debug|x86.Build.0 = Debug|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Release|x64.ActiveCfg = Release|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Release|x64.Build.0 = Release|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA}.Release|x86.Build.0 = Release|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Debug|x64.Build.0 = Debug|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Debug|x86.Build.0 = Debug|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Release|x64.ActiveCfg = Release|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Release|x64.Build.0 = Release|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Release|x86.ActiveCfg = Release|Any CPU
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676}.Release|x86.Build.0 = Release|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Debug|x64.Build.0 = Debug|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Debug|x86.Build.0 = Debug|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Release|x64.ActiveCfg = Release|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Release|x64.Build.0 = Release|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Release|x86.ActiveCfg = Release|Any CPU
+ {DF084D96-707B-47C2-9493-85FA84631ACE}.Release|x86.Build.0 = Release|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Debug|x64.Build.0 = Debug|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Debug|x86.Build.0 = Debug|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Release|x64.ActiveCfg = Release|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Release|x64.Build.0 = Release|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Release|x86.ActiveCfg = Release|Any CPU
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -84,11 +256,14 @@ Global
{C02DECC9-2A82-42C0-8F26-D0AE6559AC5E} = {B4CE3CD4-74AA-4A22-B514-BC9B380AAFD7}
{09096FEC-DA29-4914-B046-CD280220C52A} = {0295DFAC-BF6E-46C0-A63D-FBE9AF3C04E5}
{9CA13D2D-D6E2-4201-946C-81D1E6093404} = {0295DFAC-BF6E-46C0-A63D-FBE9AF3C04E5}
- {1E610F55-2D74-4856-818B-0D0B47601B75} = {0295DFAC-BF6E-46C0-A63D-FBE9AF3C04E5}
{EBFAFB5B-494F-48D5-A70D-AF1490B9260A} = {B5D35D0C-387C-44FA-9A70-6FE24DAE5728}
- {54631590-2A41-45F4-B057-92C840ED08C1} = {0EAC5155-A5EA-49C1-8E0C-19DD36D2C21C}
- {B296277A-C822-444E-8CFA-4CC4C1C1F737} = {E0861852-0F5B-4810-8586-A59038BC4034}
- {B0AB6EA7-7D53-4457-9482-F0613F99E3BB} = {0EAC5155-A5EA-49C1-8E0C-19DD36D2C21C}
+ {AB5F238F-AB78-4A85-8D8D-17E211015FD3} = {0295DFAC-BF6E-46C0-A63D-FBE9AF3C04E5}
+ {12BE663C-C0DD-4343-93DF-6B2D853B6B79} = {0295DFAC-BF6E-46C0-A63D-FBE9AF3C04E5}
+ {F64790E0-86AD-4562-9AC5-F4DD3F4881BA} = {B5D35D0C-387C-44FA-9A70-6FE24DAE5728}
+ {949C95E9-4261-416E-8D2A-F05E3D4640CA} = {64880D93-BAB4-FF83-898C-B934B68C31A9}
+ {ADDBCE30-FE7F-4198-8A37-772EF6BF3676} = {64880D93-BAB4-FF83-898C-B934B68C31A9}
+ {DF084D96-707B-47C2-9493-85FA84631ACE} = {B4CE3CD4-74AA-4A22-B514-BC9B380AAFD7}
+ {1764E3C7-CAAF-4FE9-9104-CF26D8121FC9} = {0EAC5155-A5EA-49C1-8E0C-19DD36D2C21C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AD284F35-E81F-4678-B737-A5DC8CB883CB}
diff --git a/actions/endpoint_sahil_gdbapi.yml.20241204 b/actions/endpoint_sahil_gdbapi.yml.20241204
new file mode 100644
index 000000000..94f4b0fbd
--- /dev/null
+++ b/actions/endpoint_sahil_gdbapi.yml.20241204
@@ -0,0 +1,87 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books API to Azure Web App - gdbapi
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+
+ - name: Install dotnet-ef tool
+ run: |
+ dotnet tool install --global dotnet-ef
+ echo "++++ dotnet-ef version"
+ dotnet ef --version
+
+ - name: Build with dotnet
+ run: |
+ echo "++++ dotnet build"
+ dotnet build --configuration Release
+
+ - name: Add migrations
+ run: |
+ echo "++++ current directory"
+ pwd
+ echo "++++ add ApplicationIdentityDbContext migration M1"
+ dotnet ef migrations add M1 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext --output-dir Data/Migrations/IdentityDb
+ echo "++++ add ApiDbContext migration M2"
+ dotnet ef migrations add M2 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext --output-dir Data/Migrations/ApiDb
+ echo "++++ contents of ./src/Api/Data/Migrations/IdentityDb"
+ ls ./src/Api/Data/Migrations/IdentityDb
+ echo "++++ contents of ./src/Api/Data/Migrations/ApiDb"
+ ls ./src/Api/Data/Migrations/ApiDb
+
+ - name: dotnet publish
+ run: |
+ echo "++++ contents of dotnet publish ./src/Api/Api.csproj"
+ dotnet publish ./src/Api/Api.csproj -f net8.0 -c Release -o "${{runner.temp}}/myapp"
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: ${{runner.temp}}/myapp
+
+ deploy:
+ runs-on: windows-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_A17E281C175C4E629A76134AA823BAC5 }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_258CF23452C24D9795BD94B25EF50B73 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_9375B274C69740D39F4770D5D433E8B1 }}
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'gdbapi'
+ slot-name: 'Production'
+ package: .
diff --git a/actions/endpoint_sahil_gdbmvc.yml.20241204 b/actions/endpoint_sahil_gdbmvc.yml.20241204
new file mode 100644
index 000000000..330b665ab
--- /dev/null
+++ b/actions/endpoint_sahil_gdbmvc.yml.20241204
@@ -0,0 +1,101 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books MVC project to Azure
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+ include-prerelease: true
+
+ - name: Build with dotnet
+ run: dotnet build --configuration Release
+
+ - name: dotnet publish
+ run: dotnet publish ./src/AccountGoWeb/AccountGoWeb.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp
+
+ - name: Archive production artifacts
+ run: |
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ save current directory into a variable dir ++++"
+ dir=$(pwd)
+ echo "+++++++++ what is in variable dir ++++++++++++++"
+ echo $dir
+ echo "++++++++++++++++++++++++ what's in current directory? ++++++++"
+ ls -al
+ echo "+++++ what's in the ${{env.DOTNET_ROOT}}/myapp directory? ++++"
+ ls -al ${{env.DOTNET_ROOT}}/myapp
+ echo "+++++ change directoiry to ${{env.DOTNET_ROOT}}/myapp ++++"
+ cd ${{env.DOTNET_ROOT}}/myapp
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ compress current directory and save in $dir/my_artifact.tar.gz ++++"
+ tar -czvf $dir/my_artifact.tar.gz .
+ echo "+++++++++++++++++++++++++ change dir to to $dir directory ++++"
+ cd $dir
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "++++++++++++++++++++++++ what's in $dir directory? ++++++++"
+ ls -al
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: my_artifact.tar.gz
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_8B6389BB3F37413FB2483AC2574C3BCB }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_FD62C59DE5DC42C2A07DB8191A522348 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_7076EF307FDA4C11BC99A0A7A0943794 }}
+
+ - name: Extract artifacts
+ run: |
+ tar -xzvf my_artifact.tar.gz -C .
+
+ - name: Print working directory
+ run: pwd
+
+ - name: List directory contents
+ run: ls -l /home/runner/.dotnet/
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'gdbmvc'
+ slot-name: 'Production'
+ package: .
+
\ No newline at end of file
diff --git a/actions/gdb-blazor.yml.gold b/actions/gdb-blazor.yml.gold
new file mode 100644
index 000000000..7691040f1
--- /dev/null
+++ b/actions/gdb-blazor.yml.gold
@@ -0,0 +1,76 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books BLAZOR project to Azure
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+
+ - name: Build with dotnet
+ working-directory: ./src/BlazorGDB/BlazorGDB
+ run: dotnet build --configuration Release
+
+ - name: dotnet publish
+ working-directory: ./src/BlazorGDB/BlazorGDB
+ run: dotnet publish BlazorGDB.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp
+
+ - name: sanity check
+ run: |
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "++++++++++++++++++++++++ what's in current directory? ++++++++"
+ ls -al
+ echo "+++++ what's in the ${{env.DOTNET_ROOT}}/myapp directory? ++++"
+ ls -al ${{env.DOTNET_ROOT}}/myapp
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: ${{env.DOTNET_ROOT}}/myapp
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_6A854C1CD0C74473AD2E3B9F843CC396 }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_224A065E650B4D5F9EB2329B6B2F1716 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_570B031F0942445C8E479905EE706F43 }}
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'gdb-blazor'
+ slot-name: 'Production'
+ package: .
+
\ No newline at end of file
diff --git a/actions/gdb_api.yml.flat b/actions/gdb_api.yml.flat
new file mode 100644
index 000000000..9218db27d
--- /dev/null
+++ b/actions/gdb_api.yml.flat
@@ -0,0 +1,91 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books API project to Azure
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '8.x'
+ include-prerelease: true
+
+ - name: Install dotnet-ef tool
+ run: |
+ dotnet tool install --global dotnet-ef
+ echo "++++ dotnet-ef version"
+ dotnet ef --version
+
+ - name: Build with dotnet
+ run: |
+ echo "++++ dotnet restore"
+ dotnet restore
+ echo "++++ dotnet build"
+ dotnet build --configuration Release
+
+ - name: Add migrations
+ run: |
+ echo "++++ current directory"
+ pwd
+ echo "++++ add ApplicationIdentityDbContext migration M1"
+ dotnet ef migrations add M1 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext --output-dir Data/Migrations/IdentityDb
+ echo "++++ add ApiDbContext migration M2"
+ dotnet ef migrations add M2 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext --output-dir Data/Migrations/ApiDb
+ echo "++++ contents of ./src/Api/Data/Migrations/IdentityDb"
+ ls ./src/Api/Data/Migrations/IdentityDb
+ echo "++++ contents of ./src/Api/Data/Migrations/ApiDb"
+ ls ./src/Api/Data/Migrations/ApiDb
+
+ - name: dotnet publish
+ run: |
+ echo "++++ contents of dotnet publish ./src/Api/Api.csproj"
+ dotnet publish ./src/Api/Api.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v3
+ with:
+ name: .net-app
+ path: ${{env.DOTNET_ROOT}}/myapp
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v3
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v1
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_543326D87AEF459D91E15D756166A5AC }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_D57EB2BACAA54EE2AB97F696E8E99A4B }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_3C797712E9A047958FF5C9BB540F0543 }}
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v2
+ with:
+ app-name: 'goodbooksapi'
+ slot-name: 'Production'
+ package: .
+
diff --git a/actions/gdb_mvc_tar.yml.flat b/actions/gdb_mvc_tar.yml.flat
new file mode 100644
index 000000000..0b749010a
--- /dev/null
+++ b/actions/gdb_mvc_tar.yml.flat
@@ -0,0 +1,101 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books MVC project to Azure
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+ include-prerelease: true
+
+ - name: Build with dotnet
+ run: dotnet build --configuration Release
+
+ - name: dotnet publish
+ run: dotnet publish ./src/AccountGoWeb/AccountGoWeb.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp
+
+ - name: Archive production artifacts
+ run: |
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ save current directory into a variable dir ++++"
+ dir=$(pwd)
+ echo "+++++++++ what is in variable dir ++++++++++++++"
+ echo $dir
+ echo "++++++++++++++++++++++++ what's in current directory? ++++++++"
+ ls -al
+ echo "+++++ what's in the ${{env.DOTNET_ROOT}}/myapp directory? ++++"
+ ls -al ${{env.DOTNET_ROOT}}/myapp
+ echo "+++++ change directoiry to ${{env.DOTNET_ROOT}}/myapp ++++"
+ cd ${{env.DOTNET_ROOT}}/myapp
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ compress current directory and save in $dir/my_artifact.tar.gz ++++"
+ tar -czvf $dir/my_artifact.tar.gz .
+ echo "+++++++++++++++++++++++++ change dir to to $dir directory ++++"
+ cd $dir
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "++++++++++++++++++++++++ what's in $dir directory? ++++++++"
+ ls -al
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: my_artifact.tar.gz
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_28108B2CCE81480BB0295B2554B37231 }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_9ED1B649A03F45E7B34C3BE1217B6BDE }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_A41842A963384E4BAB26580EEFE65E92 }}
+
+ - name: Extract artifacts
+ run: |
+ tar -xzvf my_artifact.tar.gz -C .
+
+ - name: Print working directory
+ run: pwd
+
+ - name: List directory contents
+ run: ls -l /home/runner/.dotnet/
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'good-books'
+ slot-name: 'Production'
+ package: .
+
diff --git a/actions/gdbblazor.yml b/actions/gdbblazor.yml
new file mode 100644
index 000000000..a372a08c3
--- /dev/null
+++ b/actions/gdbblazor.yml
@@ -0,0 +1,67 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy Good Deed Books BLAZOR project to Azure
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore ./src/BlazorGDB/BlazorGDB/BlazorGDB.csproj
+
+ - name: Build
+ run: dotnet build ./src/BlazorGDB/BlazorGDB/BlazorGDB.csproj --configuration Release
+
+ - name: Publish
+ run: dotnet publish ./src/BlazorGDB/BlazorGDB/BlazorGDB.csproj --configuration Release --output ${{ github.workspace }}/myapp
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v3
+ with:
+ name: .net-app
+ path: ${{ github.workspace }}/myapp
+
+ deploy:
+ runs-on: windows-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v3
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_C7C01847F7FC4BBFB72DEAC64242E5A4 }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_21069DC407434A3591399953BE45ED78 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_CC5D4E473B8345BA854EA230A48D8D20 }}
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v3
+ with:
+ app-name: 'gdbblazor'
+ slot-name: 'Production'
+ package: .
+
diff --git a/actions/good-books_mvc.yml.disable b/actions/good-books_mvc.yml.disable
new file mode 100644
index 000000000..659083c9a
--- /dev/null
+++ b/actions/good-books_mvc.yml.disable
@@ -0,0 +1,75 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build and deploy GoodBooks MVC project to Azure
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '8.x'
+ include-prerelease: true
+
+ - name: Build with dotnet
+ run: dotnet build --configuration Release
+
+ - name: dotnet publish
+ run: dotnet publish ./src/AccountGoWeb/AccountGoWeb.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp
+
+ # - name: Archive production artifacts
+ # run: |
+ # tar -czvf my_artifact.tar.gz ${{env.DOTNET_ROOT}}/myapp
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v3
+ with:
+ name: .net-app
+ # path: my_artifact.tar.gz
+ path: ${{env.DOTNET_ROOT}}/myapp
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v3
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v1
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_28108B2CCE81480BB0295B2554B37231 }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_9ED1B649A03F45E7B34C3BE1217B6BDE }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_A41842A963384E4BAB26580EEFE65E92 }}
+
+ # - name: Extract artifacts
+ # run: |
+ # tar -xzvf my_artifact.tar.gz -C .
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v2
+ with:
+ app-name: 'good-books'
+ slot-name: 'Production'
+ package: .
+
diff --git a/actions/good-books_react.yml.disable b/actions/good-books_react.yml.disable
new file mode 100644
index 000000000..99103e5e5
--- /dev/null
+++ b/actions/good-books_react.yml.disable
@@ -0,0 +1,54 @@
+name: Azure Static Web Apps CI/CD
+
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - endpoint_sahil
+
+jobs:
+ build_and_deploy_job:
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
+ runs-on: ubuntu-latest
+ name: Build and Deploy Job
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ submodules: true
+ lfs: false
+
+ - name: Replace API URL
+ run: |
+ echo "++++ search & replace API URL from http://localhost:8001 to https://goodbooksapi.azurewebsites.net"
+ sed -i 's|http://localhost:8001|https://goodbooksapi.azurewebsites.net|g' ./src/GoodBooksReact/src/components/Shared/Config/index.tsx
+ echo "++++ display contents of index.tsx after search & replace"
+ cat ./src/GoodBooksReact/src/components/Shared/Config/index.tsx
+
+ - name: Build And Deploy
+ id: builddeploy
+ uses: Azure/static-web-apps-deploy@v1
+ with:
+ azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_GLACIER_0EDFEC41E }}
+ repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
+ action: "upload"
+ ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
+ # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
+ app_location: "/src/GoodBooksReact/" # App source code path
+ api_location: "" # Api source code path - optional
+ output_location: "/dist" # Built app content directory - optional
+ ###### End of Repository/Build Configurations ######
+
+ close_pull_request_job:
+ if: github.event_name == 'pull_request' && github.event.action == 'closed'
+ runs-on: ubuntu-latest
+ name: Close Pull Request Job
+ steps:
+ - name: Close Pull Request
+ id: closepullrequest
+ uses: Azure/static-web-apps-deploy@v1
+ with:
+ azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_GLACIER_0EDFEC41E }}
+ action: "close"
diff --git a/actions/mvc_tar.yml.works b/actions/mvc_tar.yml.works
new file mode 100644
index 000000000..0e57f5cfc
--- /dev/null
+++ b/actions/mvc_tar.yml.works
@@ -0,0 +1,92 @@
+# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+name: Build and deploy Good Deed Books MVC project to Azure
+on:
+ push:
+ branches:
+ - endpoint_sahil
+ workflow_dispatch:
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+ include-prerelease: true
+
+ - name: Build with dotnet
+ run: dotnet build --configuration Release
+
+ - name: dotnet publish
+ run: dotnet publish ./src/AccountGoWeb/AccountGoWeb.csproj -c Release -o ${{runner.temp}}/myapp
+
+ - name: Archive production artifacts
+ run: |
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ save current directory into a variable dir ++++"
+ dir=$(pwd)
+ echo "+++++++++ what is in variable dir ++++++++++++++"
+ echo $dir
+ echo "++++++++++++++++++++++++ what's in current directory? ++++++++"
+ ls -al
+ echo "+++++ what's in the ${{runner.temp}}/myapp directory? ++++"
+ ls -al ${{runner.temp}}/myapp
+ echo "+++++ change directory to ${{runner.temp}}/myapp ++++"
+ cd ${{runner.temp}}/myapp
+ echo "+++++++++++++++++++++++++ where am I? ++++++++++++++++++++++++"
+ pwd
+ echo "+++++++++++++++++++++++++ compress current directory and save in $dir/my_artifact.tar.gz ++++"
+ tar -czvf $dir/my_artifact.tar.gz .
+ echo "+++++++++++++++++++++++++ change back to $dir directory ++++"
+ cd $dir
+ echo "++++++++++++++++++++++++ what's in $dir directory? ++++++++"
+ ls -al
+
+ - name: Upload artifact for deployment job
+ uses: actions/upload-artifact@v4
+ with:
+ name: .net-app
+ path: my_artifact.tar.gz
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: 'Production'
+ url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
+ permissions:
+ id-token: write #This is required for requesting the JWT
+
+ steps:
+ - name: Download artifact from build job
+ uses: actions/download-artifact@v4
+ with:
+ name: .net-app
+
+ - name: Login to Azure
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_8B6389BB3F37413FB2483AC2574C3BCB }}
+ tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_FD62C59DE5DC42C2A07DB8191A522348 }}
+ subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_7076EF307FDA4C11BC99A0A7A0943794 }}
+
+ - name: Extract artifacts
+ run: |
+ tar -xzvf my_artifact.tar.gz -C .
+
+ - name: Set startup command
+ run: |
+ az webapp config set --resource-group goodbooks-RG --name gdbmvc --startup-file "dotnet /home/site/wwwroot/GoodBooks.dll"
+
+ - name: Deploy to Azure Web App
+ id: deploy-to-webapp
+ uses: azure/webapps-deploy@v2
+ with:
+ app-name: 'gdbmvc'
+ slot-name: 'Production'
+ package: .
diff --git a/db/scripts/initial_data/3_InitialData-0001-Audit.sql b/db/scripts/initial_data/3_InitialData-0001-Audit.sql
new file mode 100644
index 000000000..b16be7c0d
--- /dev/null
+++ b/db/scripts/initial_data/3_InitialData-0001-Audit.sql
@@ -0,0 +1,13 @@
+-- Add audit data for the Company table
+INSERT INTO [dbo].[AuditableEntity] ([EntityName], [EnableAudit]) VALUES ('Company', 1);
+
+DECLARE @auditableEntityId INT;
+SELECT @auditableEntityId = [Id] FROM [dbo].[AuditableEntity] WHERE [EntityName] = 'Company';
+
+-- Add attributes for the Company table
+INSERT INTO [dbo].[AuditableAttribute] ([AuditableEntityId], [AttributeName], [EnableAudit])
+VALUES
+ (@auditableEntityId, 'CompanyCode', 1),
+ (@auditableEntityId, 'Name', 1),
+ (@auditableEntityId, 'ShortName', 1),
+ (@auditableEntityId, 'CRA', 1);
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index c7fef8216..75fe7a32d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,5 @@
version: "3"
-services:
+services:
api:
image: accountgo/accountgoapi
build:
@@ -8,14 +8,7 @@ services:
ports:
- "8001:8001"
environment:
- # - ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8001
- # - DBSERVER=localhost
- # - DBUSERID=dbuser
- # - DBPASSWORD=Str0ngPassword
- # - DBNAME=accountgodb
- # depends_on:
- # - db
web:
image: accountgo/accountgoweb
build:
@@ -24,13 +17,18 @@ services:
ports:
- "8000:8000"
environment:
- # - ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8000
- APIHOST=api
- # db:
- # image: microsoft/mssql-server-linux
- # ports:
- # - "1433:1433"
- # environment:
- # SA_PASSWORD: "Str0ngPassword"
- # ACCEPT_EULA: "Y"
\ No newline at end of file
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server:2022-latest
+ container_name: gdb-sql-server
+ environment:
+ - ACCEPT_EULA=Y
+ - MSSQL_SA_PASSWORD=YourStrong!Passw0rd
+ ports:
+ - "1433:1433"
+ volumes:
+ - sqlserver_data:/var/opt/mssql
+
+volumes:
+ sqlserver_data:
\ No newline at end of file
diff --git a/docs/Bootstrap Blazor.txt b/docs/Bootstrap Blazor.txt
new file mode 100644
index 000000000..97341be99
--- /dev/null
+++ b/docs/Bootstrap Blazor.txt
@@ -0,0 +1,93 @@
+https://github.com/vikramlearning/blazorbootstrap-starter-templates/tree/master
+
+
+dotnet add package Blazor.Bootstrap -v 3.0.0-preview.2
+
+Program.cs
+
+ builder.Services.AddBlazorBootstrap(); // Add this line
+
+_Imports.razor
+
+ @using BlazorBootstrap;
+
+Delete wwwroot/bootstrap folder
+
+Replace MainLayout.razor with:
+
+ @inherits LayoutComponentBase
+
+
+
+
+
+
+
+
+
+ @Body
+
+
+
+
+
+ @code {
+ Sidebar sidebar;
+ IEnumerable navItems;
+
+ private async Task SidebarDataProvider(SidebarDataProviderRequest request)
+ {
+ if (navItems is null)
+ navItems = GetNavItems();
+
+ return await Task.FromResult(request.ApplyTo(navItems));
+ }
+
+ private IEnumerable GetNavItems()
+ {
+ navItems = new List
+ {
+ new NavItem { Id = "1", Href = "/", IconName = IconName.HouseDoorFill, Text = "Home", Match=NavLinkMatch.All},
+ new NavItem { Id = "2", Href = "/counter", IconName = IconName.PlusSquareFill, Text = "Counter"},
+ new NavItem { Id = "3", Href = "/weather", IconName = IconName.Table, Text = "Fetch Data"},
+ };
+
+ return navItems;
+ }
+ }
+
+
+
+ An unhandled error has occurred.
+
Reload
+
🗙
+
+
+App.razor
+
+ 1) Delete >>
+ 2) Add these lines at top of file under
+
+
+
+
+
+ 3) Add these lines at bottom of file under
+
+
+
+
+
+
+
+
+ 4) Change to:
+
+
+
+
\ No newline at end of file
diff --git a/docs/GoodDeedBooks.docx b/docs/GoodDeedBooks.docx
new file mode 100644
index 000000000..30ae3bee2
Binary files /dev/null and b/docs/GoodDeedBooks.docx differ
diff --git a/docs/README.md b/docs/README.md
index 74552fce9..c4965d29f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -128,7 +128,7 @@ At this point, your database has no data on it. But there is already an initial
- Items
- Banks
-To initialize a company, call the api endpoint directly http://localhost:8001/api/administration/initializedcompany from the browser or by using curl e.g. `curl http://localhost:8001/api/administration/initializedcompany`. If you encounter some issues, the easy way for now is recreate your database and repeat the `Publish Database` section.
+To initialize a company, call the api endpoint directly http://localhost:8001/api/administration/setup from the browser or by using curl e.g. `curl http://localhost:8001/api/administration/setup`. If you encounter some issues, the easy way for now is recreate your database and repeat the `Publish Database` section.
## Build and Run "Api" (Back-end)
1. Navigate directory to `src/Api` project
@@ -181,7 +181,7 @@ To run everything (database, api, web) in docker container you can use docker-co
1. Database instance running in docker container and you can connect to it
1. You should have a running "Api" and can test it by getting the list of customers e.g. http://localhost:8001/api/sales customers
1. You can browse the UI from http://localhost:8000 and able to login to the system using initial username/password: admin@accountgo.ph/P@ssword1
-1. Initialize data by calling a special api endpoint directly. http://localhost:8001/api/administration/initializedcompany
+1. Initialize data by calling a special api endpoint directly: http://localhost:8001/api/administration/setup
# Technology Stack
- ASP.NET Core 3.1
@@ -207,4 +207,4 @@ If you are a developer and wanted to take part as contributor/collaborator we ar
So go ahead, add your code and make your first pull request.
# Contact Support
-Feel free to email mvpsolution@gmail.com of any questions.
\ No newline at end of file
+Feel free to email mvpsolution@gmail.com of any questions.
diff --git a/docs/azure.txt b/docs/azure.txt
new file mode 100644
index 000000000..1e437cd3d
--- /dev/null
+++ b/docs/azure.txt
@@ -0,0 +1,8 @@
+API:
+https://goodbooksapi.azurewebsites.net
+
+MVC:
+https://good-books.azurewebsites.net
+
+React:
+https://mango-glacier-0edfec41e.5.azurestaticapps.net
diff --git a/docs/background.txt b/docs/background.txt
new file mode 100644
index 000000000..dfd5d92f3
--- /dev/null
+++ b/docs/background.txt
@@ -0,0 +1,41 @@
+I was asked by a non-profit organization to help them find a cheap accounting system instead of paying high subscription fees from a current vendor. I stumbled upon this open source project on GitHub:
+
+https://github.com/AccountGo/accountgo
+
+It is based on the following technologies:
+
+Backend: ASP.NET WebAPI and MVC
+Frontend: React with TypeScript
+Database: SQL Server
+
+It seems that development on this app stopped about seven years ago. When I looked at it, I figured that it has most of what is needed and could be brought up to snuff by upgrading the application to the latest state of .NET and React. Therefore, I forked it and updated it to the latest versions of .NET, React, and TypeScript.
+
+The forked app is at https://github.com/medhatelmasry/GoodBooks
+
+You can run it by following these steps:
+
+Clone the repo
+Start SQL Server in a docker container with:
+
+ docker run --cap-add SYS_PTRACE -e ACCEPT_EULA=1 -e MSSQL_SA_PASSWORD=SqlPassword! -p 1444:1433 --name azsql -d mcr.microsoft.com/azure-sql-edge
+
+In root directory of the code, run the following commands:
+
+ dotnet ef migrations add M1 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext --output-dir Data/Migrations/IdentityDb
+
+ dotnet ef migrations add M2 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext --output-dir Data/Migrations/ApiDb
+
+ dotnet ef database update --project ./src/Api/ --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext
+
+ dotnet ef database update --project ./src/Api/ --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext
+
+Update to the latest versions of Node & Npm
+Go to the src/Api folder and start the WebAPI app with: dotnet watch
+Hit this endpoint in order to populate the database with some sample data: http://localhost:8001/api/administration/setup
+In a separate terminal window, go to the src/GoodBooksReact folder run these commands:
+
+ npm install
+ npm run dev
+
+The React app will run. It is a rudimentary frontend menu system and is a work in progress.
+
diff --git a/docs/expand-chart-of-accounts.docx b/docs/expand-chart-of-accounts.docx
new file mode 100644
index 000000000..0ebf39cf4
Binary files /dev/null and b/docs/expand-chart-of-accounts.docx differ
diff --git a/docs/medhat.txt b/docs/medhat.txt
new file mode 100644
index 000000000..b66e10c56
--- /dev/null
+++ b/docs/medhat.txt
@@ -0,0 +1,28 @@
+docker run --cap-add SYS_PTRACE -e ACCEPT_EULA=1 -e MSSQL_SA_PASSWORD=SqlPassword! -p 1444:1433 --name azsql -d mcr.microsoft.com/azure-sql-edge
+
+Data Source=localhost,1444;Database=Northwind;Persist Security Info=True;User ID=sa;Password=SqlPassword!;TrustServerCertificate=True;
+
+====================
+
+dotnet ef migrations add M1 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext --output-dir Data/Migrations/IdentityDb
+
+dotnet ef migrations add M2 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext --output-dir Data/Migrations/ApiDb
+
+====================
+
+dotnet ef database update --project ./src/Api/ --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext
+
+dotnet ef database update --project ./src/Api/ --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext
+
+====================
+
+Update to the latest versions of node & npm
+
+====================
+
+Start the API .NET application then hit this endpoint in a browser to create seed data:
+http://localhost:8001/api/administration/setup
+
+
+
+
diff --git a/docs/open-source.txt b/docs/open-source.txt
new file mode 100644
index 000000000..38db29dbc
--- /dev/null
+++ b/docs/open-source.txt
@@ -0,0 +1,3 @@
+Open Source Accounting System
+
+https://github.com/AccountGo/accountgo
diff --git a/docs/pr.txt b/docs/pr.txt
new file mode 100644
index 000000000..b5b7b5215
--- /dev/null
+++ b/docs/pr.txt
@@ -0,0 +1,19 @@
+git checkout -b dotnet_9 origin/dotnet_9
+
+docker run --cap-add SYS_PTRACE -e ACCEPT_EULA=1 -e MSSQL_SA_PASSWORD=SqlPassword! -p 1444:1433 --name sql -d mcr.microsoft.com/mssql/server:2022-latest
+
+---------
+
+dotnet ef migrations add M1 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext --output-dir Data/Migrations/IdentityDb
+
+dotnet ef migrations add M2 --project ./src/Api/ --startup-project ./src/Api/Api.csproj --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext --output-dir Data/Migrations/ApiDb
+
+dotnet ef database update --project ./src/Api/ --msbuildprojectextensionspath .build/obj/Api/ --context ApplicationIdentityDbContext
+
+dotnet ef database update --project ./src/Api/ --msbuildprojectextensionspath .build/obj/Api/ --context ApiDbContext
+
+==========
+
+Apply Entity Framework Core migrations in .NET Aspire
+ https://learn.microsoft.com/en-us/dotnet/aspire/database/ef-core-migrations
+
diff --git a/docs/react.txt b/docs/react.txt
new file mode 100644
index 000000000..2e56c21d6
--- /dev/null
+++ b/docs/react.txt
@@ -0,0 +1,4 @@
+https://www.youtube.com/watch?v=ElgfQdq-Htk
+
+https://www.youtube.com/watch?v=oN9W0Tkn8hg
+
diff --git a/move-to-blazor.txt b/move-to-blazor.txt
new file mode 100644
index 000000000..e388316b6
--- /dev/null
+++ b/move-to-blazor.txt
@@ -0,0 +1,9 @@
+Recreate starter Blazor app with database authentication
+- we will use JWT for client-side authentication
+
+Get chart of account to work
+
+CI/CD GiHul >> Azure
+
+Meet and decide on moving the current application into the Blazor template
+
diff --git a/src/AccountGoWeb/.vscode/launch.json b/src/AccountGoWeb/.vscode/launch.json
index 5825c3616..ddd8758f9 100644
--- a/src/AccountGoWeb/.vscode/launch.json
+++ b/src/AccountGoWeb/.vscode/launch.json
@@ -4,6 +4,11 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ },
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
diff --git a/src/AccountGoWeb/AccountGoWeb.csproj b/src/AccountGoWeb/AccountGoWeb.csproj
index 0175223c6..3d681ea82 100644
--- a/src/AccountGoWeb/AccountGoWeb.csproj
+++ b/src/AccountGoWeb/AccountGoWeb.csproj
@@ -1,46 +1,37 @@
-
-
+
- net7.0
- true
- AccountGoWeb
- AccountGoWeb
- latest
- 0.0.1-alpha
- Latest
+ net10.0
+ GoodBooks
+ GoodBooks
+ 1.0.0
+ enable
+ enable
+ true
+ aspnet-GoodBooks-21ac3a7f-d42e-4136-9340-b4f6254706df
+
+ true
+ NU1701
-
PreserveNewest
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/App.razor b/src/AccountGoWeb/Components/App.razor
new file mode 100644
index 000000000..abb7c3dd7
--- /dev/null
+++ b/src/AccountGoWeb/Components/App.razor
@@ -0,0 +1,23 @@
+@*
+
+
+
+
+
+ *@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Audit/AuditableEntities.razor b/src/AccountGoWeb/Components/Pages/Audit/AuditableEntities.razor
new file mode 100644
index 000000000..3edc50471
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Audit/AuditableEntities.razor
@@ -0,0 +1,215 @@
+@page "/audit/auditable-entities-blazor"
+@namespace AccountGoWeb.Components.Pages.Audit
+@using Dto.Auditing
+@using System.Text.Json
+@inject IHttpClientFactory ClientFactory
+@inject IConfiguration Configuration
+
+Auditable Entities
+
+@if (getError)
+{
+ Unable to get data. Please try again later.
+}
+else if (isLoading)
+{
+
+
+ Loading...
+
+
Loading auditable entities...
+
+}
+else
+{
+
+
+
+
+
+
+
+
+ @if (entities == null || !entities.Any())
+ {
+
+
No auditable entities found.
+
+ }
+ else
+ {
+
+
+
+
+ ID
+ Entity Name
+ Enable Audit
+ Actions
+
+
+
+ @foreach (var entity in entities)
+ {
+
+ @entity.Id
+ @entity.EntityName
+
+ @if (entity.EnableAudit)
+ {
+
+ }
+ else
+ {
+
+ }
+
+
+
+ Edit
+
+
+ Attributes
+
+ OpenDeleteModal(entity)">
+ Delete
+
+
+
+ }
+
+
+
+ }
+
+
+
+
+}
+
+@* Delete Confirmation Modal *@
+@if (isDeleteModalVisible && selectedEntity != null)
+{
+
+
+
+
+
+
+ Are you sure you want to delete the entity
+ @selectedEntity.EntityName ?
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
@errorMessage
+ }
+
+
+
+
+
+
+}
+
+@code {
+ private List entities = new();
+ private AuditableEntity? selectedEntity = null;
+ private bool isDeleteModalVisible = false;
+ private string errorMessage = string.Empty;
+ private bool isLoading = true;
+ private bool getError = false;
+ private string apiUrl = string.Empty;
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Get API URL from configuration (same as Program.cs sets it)
+ apiUrl = Configuration["ApiUrl"]!;
+ await LoadEntitiesFromApi();
+ isLoading = false;
+ }
+
+ private async Task LoadEntitiesFromApi()
+ {
+ try
+ {
+ var client = ClientFactory.CreateClient();
+
+ var response = await client.GetAsync($"{apiUrl}audit/entities");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var jsonString = await response.Content.ReadAsStringAsync();
+ entities = JsonSerializer.Deserialize>(jsonString, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ }) ?? new List();
+ }
+ else
+ {
+ getError = true;
+ }
+ }
+ catch
+ {
+ getError = true;
+ }
+ }
+
+ private void OpenDeleteModal(AuditableEntity entity)
+ {
+ selectedEntity = entity;
+ errorMessage = string.Empty;
+ isDeleteModalVisible = true;
+ }
+
+ private void CloseDeleteModal()
+ {
+ isDeleteModalVisible = false;
+ selectedEntity = null;
+ errorMessage = string.Empty;
+ }
+
+ private async Task ConfirmDeleteEntity()
+ {
+ if (selectedEntity == null)
+ {
+ errorMessage = "No entity selected.";
+ return;
+ }
+
+ try
+ {
+ var client = ClientFactory.CreateClient();
+
+ var response = await client.DeleteAsync($"{apiUrl}audit/entity/{selectedEntity.Id}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ // Remove from local list
+ entities.Remove(selectedEntity);
+ CloseDeleteModal();
+
+ // Reload to ensure data is fresh
+ await LoadEntitiesFromApi();
+ }
+ else
+ {
+ errorMessage = "Failed to delete entity. Please try again.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error deleting entity: {ex.Message}";
+ }
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Audit/EntityForm.razor b/src/AccountGoWeb/Components/Pages/Audit/EntityForm.razor
new file mode 100644
index 000000000..b87cb0159
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Audit/EntityForm.razor
@@ -0,0 +1,153 @@
+@page "/audit/entity-form"
+@page "/audit/entity-form/{Id:int}"
+@namespace AccountGoWeb.Components.Pages.Audit
+@using Dto.Auditing
+@using System.Text.Json
+@using System.Text
+@inject IHttpClientFactory ClientFactory
+@inject IConfiguration Configuration
+@inject NavigationManager Navigation
+
+@(Id == 0 ? "Add New" : "Edit") Auditable Entity
+
+@if (isLoading)
+{
+
+}
+else
+{
+
+
+
+
+
+
+
+
+
+
+
+ Entity Name
+
+
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
@errorMessage
+ }
+
+
+
+
+
+
+
+
+}
+
+@code {
+ [Parameter]
+ public int Id { get; set; } = 0;
+
+ private AuditableEntity entity = new() { EnableAudit = true };
+ private bool isSaving = false;
+ private bool isLoading = false;
+ private string errorMessage = string.Empty;
+ private string apiUrl = string.Empty;
+
+ protected override async Task OnInitializedAsync()
+ {
+ apiUrl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+
+ if (Id > 0)
+ {
+ isLoading = true;
+ await LoadEntity();
+ isLoading = false;
+ }
+ }
+
+ private async Task LoadEntity()
+ {
+ try
+ {
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiUrl}audit/entity?id={Id}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var jsonString = await response.Content.ReadAsStringAsync();
+ entity = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ }) ?? new AuditableEntity { EnableAudit = true };
+ }
+ else
+ {
+ errorMessage = "Failed to load entity.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading entity: {ex.Message}";
+ }
+ }
+
+ private async Task HandleSubmit()
+ {
+ isSaving = true;
+ errorMessage = string.Empty;
+
+ try
+ {
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(entity);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync($"{apiUrl}audit/entity", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Navigation.NavigateTo("/Audit/GetAuditableEntities", forceLoad: true);
+ }
+ else
+ {
+ errorMessage = "Failed to save entity. Please try again.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving entity: {ex.Message}";
+ }
+ finally
+ {
+ isSaving = false;
+ }
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Contact/Contact.razor b/src/AccountGoWeb/Components/Pages/Contact/Contact.razor
new file mode 100644
index 000000000..a436d3d74
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Contact/Contact.razor
@@ -0,0 +1,230 @@
+@using ContactDto = Dto.Common.Contact
+@inject HttpClient Http
+@inject NavigationManager Navigation
+@rendermode InteractiveServer
+
+@(Id == 0 ? "New" : "Edit") Contact
+
+
+
+
+
+
+ @if (Id != 0 && !isEditMode)
+ {
+
+ Edit
+
+ }
+
+
+
+ @if (isLoading)
+ {
+
+
+
+ Loading...
+
+
Loading contact...
+
+
+ }
+ else if (errorMessage != null)
+ {
+
+ }
+ else
+ {
+
+
+
+
+
+
+
+
+ First Name
+
+
+
+
+
+
+
+
+ Last Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (isEditMode || Id == 0)
+ {
+
+ Save
+
+ }
+
+ Close
+
+
+
+
+ }
+
+
+@code {
+ [Parameter]
+ public int Id { get; set; } = 0;
+
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public int? PartyId { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public int? PartyType { get; set; }
+
+ private ContactDto Model { get; set; } = new();
+
+ private bool isLoading = true;
+ private bool isEditMode = false;
+ private string? errorMessage;
+
+ protected override async Task OnInitializedAsync()
+ {
+ isLoading = true;
+
+ if (Id != 0)
+ {
+ await LoadContact();
+ }
+ else
+ {
+ isEditMode = true;
+ Model.HoldingPartyId = PartyId.GetValueOrDefault();
+ Model.HoldingPartyType = PartyType.GetValueOrDefault();
+ }
+
+ isLoading = false;
+ }
+
+ private async Task LoadContact()
+ {
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = $"{baseApiUrl}contact/contact?id={Id}";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Model = await response.Content.ReadFromJsonAsync() ?? new();
+ }
+ else
+ {
+ errorMessage = $"Failed to load contact. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading contact: {ex.Message}";
+ }
+ }
+
+ private async Task HandleValidSubmit()
+ {
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + "contact/savecontact";
+
+ var response = await Http.PostAsJsonAsync(apiUrl, Model);
+
+ if (response.IsSuccessStatusCode)
+ {
+ if (PartyId.HasValue && PartyType.HasValue)
+ {
+ Navigation.NavigateTo($"/contact/contacts?partyId={PartyId}&partyType={PartyType}", forceLoad: true);
+ }
+ else
+ {
+ Navigation.NavigateTo("/sales/customers", forceLoad: true);
+ }
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save contact. Status: {response.StatusCode}. {errorContent}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving contact: {ex.Message}";
+ }
+ }
+
+ private void EnableEditMode()
+ {
+ isEditMode = true;
+ }
+
+ private void NavigateBack()
+ {
+ if (PartyId.HasValue && PartyType.HasValue)
+ {
+ Navigation.NavigateTo($"/contact/contacts?partyId={PartyId}&partyType={PartyType}", forceLoad: true);
+ }
+ else if (PartyId.HasValue)
+ {
+ Navigation.NavigateTo($"/sales/customer/{PartyId}", forceLoad: true);
+ }
+ else
+ {
+ Navigation.NavigateTo("/sales/customers", forceLoad: true);
+ }
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Contact/Contacts.razor b/src/AccountGoWeb/Components/Pages/Contact/Contacts.razor
new file mode 100644
index 000000000..0cd019779
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Contact/Contacts.razor
@@ -0,0 +1,340 @@
+@using ContactDto = Dto.Common.Contact
+@inject HttpClient Http
+@inject NavigationManager Navigation
+@rendermode InteractiveServer
+
+Contacts
+
+
+
+
+
+
+
+ New Contact
+
+ @if (selectedContact != null)
+ {
+
+ View
+
+
+ Set as Primary Contact
+
+ }
+
+ Back to Customers
+
+
+
+
+ @if (successMessage != null)
+ {
+
+
+
+ @successMessage
+ successMessage = null">
+
+
+
+ }
+
+ @if (isLoading)
+ {
+
+
+
+ Loading...
+
+
Loading contacts...
+
+
+ }
+ else if (errorMessage != null)
+ {
+
+ }
+ else if (contacts == null || !contacts.Any())
+ {
+
+
+
+ No contacts found.
+
+
+
+ }
+ else
+ {
+
+
+
+
+
+
+ SortBy(nameof(ContactDto.Id))" style="cursor: pointer;">
+ Id @GetSortIcon(nameof(ContactDto.Id))
+
+ SortBy(nameof(ContactDto.FirstName))" style="cursor: pointer;">
+ First Name @GetSortIcon(nameof(ContactDto.FirstName))
+
+ SortBy(nameof(ContactDto.LastName))" style="cursor: pointer;">
+ Last Name @GetSortIcon(nameof(ContactDto.LastName))
+
+ Primary
+
+
+
+ @foreach (var contact in contacts)
+ {
+ SelectContact(contact)"
+ style="cursor: pointer; @(selectedContact?.Id == contact.Id ? "background-color: #007bff33; border-left: 3px solid #007bff;" : "")">
+
+
+ @contact.Id
+
+
+ @contact.FirstName
+ @contact.LastName
+
+ @if (IsPrimaryContact(contact))
+ {
+
+ Primary
+
+ }
+
+
+ }
+
+
+
+
+
+
+
+
+
Total: @contacts.Count() contact(s)
+
+
+ }
+
+
+@code {
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public int? PartyId { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public int? PartyType { get; set; }
+
+ private List? contacts;
+ private ContactDto? selectedContact;
+ private Dto.Sales.Customer? currentCustomer;
+ private bool isLoading = true;
+ private string? errorMessage;
+ private string? successMessage;
+ private string? sortColumn;
+ private bool sortAscending = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadCustomer();
+ await LoadContacts();
+ }
+
+ private async Task LoadCustomer()
+ {
+ if (!PartyId.HasValue) return;
+
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var customerUrl = $"{baseApiUrl}sales/customer?id={PartyId}";
+ var customerResponse = await Http.GetAsync(customerUrl);
+
+ if (customerResponse.IsSuccessStatusCode)
+ {
+ currentCustomer = await customerResponse.Content.ReadFromJsonAsync();
+ }
+ }
+ catch (Exception)
+ {
+ // Silently fail - customer may not exist yet
+ }
+ }
+
+ private async Task LoadContacts()
+ {
+ isLoading = true;
+ errorMessage = null;
+
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + $"contact/contacts?partyId={PartyId}&partyType={PartyType}";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ contacts = await response.Content.ReadFromJsonAsync>();
+ }
+ else
+ {
+ errorMessage = $"Failed to load contacts. Status: {response.StatusCode}";
+ }
+ }
+ catch (HttpRequestException ex)
+ {
+ errorMessage = $"Network error: {ex.Message}. Please ensure the API is running on port 8001.";
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading contacts: {ex.Message}";
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SelectContact(ContactDto contact)
+ {
+ selectedContact = contact;
+ }
+
+ private async Task SetAsPrimaryContact()
+ {
+ if (selectedContact == null || !PartyId.HasValue) return;
+
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ // Load the customer
+ var customerUrl = $"{baseApiUrl}sales/customer?id={PartyId}";
+ var customerResponse = await Http.GetAsync(customerUrl);
+
+ if (customerResponse.IsSuccessStatusCode)
+ {
+ var customer = await customerResponse.Content.ReadFromJsonAsync();
+ if (customer != null)
+ {
+ // Update customer's primary contact data
+ if (customer.PrimaryContact == null)
+ {
+ customer.PrimaryContact = new ContactDto();
+ }
+ customer.PrimaryContact.FirstName = selectedContact.FirstName;
+ customer.PrimaryContact.LastName = selectedContact.LastName;
+ customer.PrimaryContact.Party = selectedContact.Party;
+
+ // Save the customer
+ var saveUrl = baseApiUrl + "sales/savecustomer";
+ var saveResponse = await Http.PostAsJsonAsync(saveUrl, customer);
+
+ if (saveResponse.IsSuccessStatusCode)
+ {
+ successMessage = $"Set {selectedContact.FirstName} {selectedContact.LastName} as primary contact.";
+ currentCustomer = customer;
+ StateHasChanged();
+ }
+ else
+ {
+ errorMessage = $"Failed to update primary contact. Status: {saveResponse.StatusCode}";
+ }
+ }
+ }
+ else
+ {
+ errorMessage = $"Failed to load customer. Status: {customerResponse.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error setting primary contact: {ex.Message}";
+ }
+ }
+
+ private void NavigateToViewContact()
+ {
+ if (selectedContact != null)
+ {
+ Navigation.NavigateTo($"/contact/contact/{selectedContact.Id}?partyId={selectedContact.HoldingPartyId}&partyType={selectedContact.HoldingPartyType}", forceLoad: true);
+ }
+ }
+
+ private void NavigateToNewContact()
+ {
+ if (PartyId.HasValue && PartyType.HasValue)
+ {
+ Navigation.NavigateTo($"/contact/contact?partyId={PartyId}&partyType={PartyType}", forceLoad: true);
+ }
+ else
+ {
+ Navigation.NavigateTo("/contact/contact", forceLoad: true);
+ }
+ }
+
+ private void NavigateToCustomers()
+ {
+ if (PartyId.HasValue)
+ {
+ Navigation.NavigateTo($"/sales/customer/{PartyId}", forceLoad: true);
+ }
+ else
+ {
+ Navigation.NavigateTo("/sales/customers", forceLoad: true);
+ }
+ }
+
+ private void SortBy(string column)
+ {
+ if (contacts == null) return;
+
+ if (sortColumn == column)
+ {
+ sortAscending = !sortAscending;
+ }
+ else
+ {
+ sortColumn = column;
+ sortAscending = true;
+ }
+
+ contacts = column switch
+ {
+ nameof(ContactDto.Id) => sortAscending
+ ? contacts.OrderBy(c => c.Id).ToList()
+ : contacts.OrderByDescending(c => c.Id).ToList(),
+ nameof(ContactDto.FirstName) => sortAscending
+ ? contacts.OrderBy(c => c.FirstName).ToList()
+ : contacts.OrderByDescending(c => c.FirstName).ToList(),
+ nameof(ContactDto.LastName) => sortAscending
+ ? contacts.OrderBy(c => c.LastName).ToList()
+ : contacts.OrderByDescending(c => c.LastName).ToList(),
+ _ => contacts
+ };
+ }
+
+ private string GetSortIcon(string column)
+ {
+ if (sortColumn != column) return "";
+ return sortAscending ? "▲" : "▼";
+ }
+
+ private bool IsPrimaryContact(ContactDto contact)
+ {
+ if (currentCustomer?.PrimaryContact == null) return false;
+ return currentCustomer.PrimaryContact.FirstName == contact.FirstName &&
+ currentCustomer.PrimaryContact.LastName == contact.LastName;
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Counter.razor b/src/AccountGoWeb/Components/Pages/Counter.razor
new file mode 100644
index 000000000..0d9d43ad4
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Counter.razor
@@ -0,0 +1,19 @@
+@page "/counter"
+@rendermode InteractiveServer
+
+Counter
+
+Counter
+
+Current count: @currentCount
+
+Click me
+
+@code {
+ private int currentCount = 0;
+
+ private void IncrementCount()
+ {
+ currentCount++;
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Donations/AddDonationInvoice.razor b/src/AccountGoWeb/Components/Pages/Donations/AddDonationInvoice.razor
new file mode 100644
index 000000000..5290a4daa
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Donations/AddDonationInvoice.razor
@@ -0,0 +1,476 @@
+@rendermode InteractiveServer
+@using AspNetCoreGeneratedDocument
+@using Dto.Donations
+@using Dto.Sales
+@using Dto.Inventory
+@using System.Text.Json
+@inject HttpClient Http
+@inject IConfiguration Configuration
+@inject NavigationManager NavigationManager
+
+
+
+Add Donation Invoice
+
+
+
+@if (!string.IsNullOrEmpty(errorMessage))
+{
+
+ @errorMessage
+ errorMessage = null">
+
+}
+
+@if (!string.IsNullOrEmpty(successMessage))
+{
+
+ @successMessage
+ successMessage = null">
+
+}
+
+@if (isLoading)
+{
+
+}
+else if (invoice != null)
+{
+
+
+
+
+
+
+
+
Invoice Lines
+
+ Add Line
+
+
+
+
+
+
+
+ Item
+ Quantity
+ Amount
+ Measurement
+ Notes
+
+
+
+
+ @for (int i = 0; i < (invoice.DonationInvoiceLines?.Count ?? 0); i++)
+ {
+ var index = i;
+ var line = invoice.DonationInvoiceLines![index];
+
+
+
+ -- Select Item --
+ @foreach (var item in items)
+ {
+ @item.Description
+ }
+
+
+
+
+
+
+
+
+
+
+ -- Select --
+ @foreach (var measurement in measurements)
+ {
+ @measurement.Description
+ }
+
+
+
+
+
+
+ @if (invoice.DonationInvoiceLines.Count > 1)
+ {
+ RemoveLine(index)">
+ Remove
+
+ }
+
+
+ }
+
+
+
+
+
+ Total: @invoice.Amount.ToString("C2")
+
+
+
+
+
+ @if (isSaving)
+ {
+
+ }
+ Save
+
+
+ Cancel
+
+
+
+}
+
+@code {
+ private DonationInvoice? invoice;
+ private List customers = new();
+ private List- items = new();
+ private List
measurements = new();
+ private bool isLoading = true;
+ private bool isSaving = false;
+ private string? errorMessage;
+ private string? successMessage;
+
+ public class Measurement
+ {
+ public int Id { get; set; }
+ public string? Code { get; set; }
+ public string? Description { get; set; }
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadData();
+ }
+
+ private async Task LoadData()
+ {
+ isLoading = true;
+ try
+ {
+ var apiUrl = Configuration["ApiUrl"];
+
+ // Load customers
+ var customersResponse = await Http.GetAsync($"{apiUrl}sales/customers");
+ if (customersResponse.IsSuccessStatusCode)
+ {
+ var customersJson = await customersResponse.Content.ReadAsStringAsync();
+ customers = JsonSerializer.Deserialize>(customersJson,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
+ }
+
+ // Load items
+ var itemsResponse = await Http.GetAsync($"{apiUrl}inventory/items");
+ if (itemsResponse.IsSuccessStatusCode)
+ {
+ var itemsJson = await itemsResponse.Content.ReadAsStringAsync();
+ items = JsonSerializer.Deserialize>(itemsJson,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
+ }
+
+ // Load measurements
+ var measurementsResponse = await Http.GetAsync($"{apiUrl}common/measurements");
+ if (measurementsResponse.IsSuccessStatusCode)
+ {
+ var measurementsJson = await measurementsResponse.Content.ReadAsStringAsync();
+ measurements = JsonSerializer.Deserialize>(measurementsJson,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
+ }
+
+ // Initialize invoice
+ invoice = new DonationInvoice
+ {
+ No = new Random().Next(1, 99999).ToString(),
+ DonationDate = DateTime.Now,
+ DonationInvoiceLines = new List
+{
+new DonationInvoiceLine
+{
+Amount = 0,
+Quantity = 1,
+ItemId = items.FirstOrDefault()?.Id ?? 0,
+MeasurementId = measurements.FirstOrDefault()?.Id ?? 0
+}
+}
+ };
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading data: {ex.Message}";
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void AddLine()
+ {
+ invoice?.DonationInvoiceLines?.Add(new DonationInvoiceLine
+ {
+ Amount = 0,
+ Quantity = 1,
+ ItemId = items.FirstOrDefault()?.Id ?? 0,
+ MeasurementId = measurements.FirstOrDefault()?.Id ?? 0
+ });
+ }
+
+ private void RemoveLine(int index)
+ {
+ if (invoice?.DonationInvoiceLines != null && invoice.DonationInvoiceLines.Count > 1)
+ {
+ invoice.DonationInvoiceLines.RemoveAt(index);
+ }
+ }
+
+ private async Task SaveInvoice()
+ {
+ if (invoice == null) return;
+
+ isSaving = true;
+ errorMessage = null;
+ successMessage = null;
+
+ try
+ {
+ var apiUrl = Configuration["ApiUrl"];
+ var json = JsonSerializer.Serialize(invoice);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await Http.PostAsync($"{apiUrl}Donations/CreateDonationInvoice", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ successMessage = $"Donation invoice '{invoice.No}' saved successfully!";
+ ResetForm();
+
+ // Auto-navigate after 2 seconds
+ await Task.Delay(2000);
+ NavigationManager.NavigateTo("/donations/donationinvoices");
+ }
+ else
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save: {response.StatusCode} - {error}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving invoice: {ex.Message}";
+ }
+ finally
+ {
+ isSaving = false;
+ }
+ }
+ private void ResetForm()
+ {
+ invoice = new DonationInvoice
+ {
+ No = new Random().Next(1, 99999).ToString(),
+ DonationDate = DateTime.Now,
+ DonationInvoiceLines = new List
+{
+new DonationInvoiceLine
+{
+Amount = 0,
+Quantity = 1,
+ItemId = items.FirstOrDefault()?.Id ?? 0,
+MeasurementId = measurements.FirstOrDefault()?.Id ?? 0
+}
+}
+ };
+ }
+
+ private void Cancel()
+ {
+ NavigationManager.NavigateTo("/donations/donationinvoices");
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Donations/DonationInvoices.razor b/src/AccountGoWeb/Components/Pages/Donations/DonationInvoices.razor
new file mode 100644
index 000000000..626e0fbbf
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Donations/DonationInvoices.razor
@@ -0,0 +1,370 @@
+@rendermode InteractiveServer
+@using AspNetCoreGeneratedDocument
+@using Dto.Donations
+@inject HttpClient Http
+@inject IConfiguration Configuration
+@inject NavigationManager NavigationManager
+@inject IJSRuntime JSRuntime
+
+
+
+Donation Invoices
+
+
+
+
+
+
+@if (isLoading)
+{
+
+}
+else if (donationInvoices != null && donationInvoices.Any())
+{
+
+}
+else
+{
+
+
+
+ No donation invoices found. Click "New Donation Invoice" to create one.
+
+
+}
+
+@* Delete Confirmation Modal *@
+@if (showDeleteModal)
+{
+
+
+
+
+
+
Do you want to delete this donation invoice?
+ @if (selectedInvoice != null)
+ {
+
Invoice No: @selectedInvoice.No
+
Donor: @selectedInvoice.DonorName
+
Amount: @selectedInvoice.Amount.ToString("C2")
+ }
+
+
+
+
+
+}
+
+
+
+@code {
+ private List? donationInvoices;
+ private DonationInvoice? selectedInvoice;
+ private bool isLoading = true;
+ private bool showDeleteModal = false;
+ private bool HasSelection => selectedInvoice != null;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadDonationInvoices();
+ }
+
+ private async Task LoadDonationInvoices()
+ {
+ isLoading = true;
+ try
+ {
+ var apiUrl = Configuration["ApiUrl"];
+ var response = await Http.GetAsync($"{apiUrl}donations/donationinvoices");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseJson = await response.Content.ReadAsStringAsync();
+ donationInvoices = System.Text.Json.JsonSerializer.Deserialize>(
+ responseJson,
+ new System.Text.Json.JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ }
+ else
+ {
+ donationInvoices = new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error loading donation invoices: {ex.Message}");
+ donationInvoices = new List();
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SelectInvoice(DonationInvoice invoice)
+ {
+ selectedInvoice = invoice;
+ StateHasChanged();
+ }
+
+ private void EditInvoice()
+ {
+ if (selectedInvoice != null)
+ {
+ NavigationManager.NavigateTo($"/donations/donationinvoice?id={selectedInvoice.Id}");
+ }
+ }
+
+ private void ShowDeleteModal()
+ {
+ if (selectedInvoice != null)
+ {
+ showDeleteModal = true;
+ }
+ }
+
+ private void CloseDeleteModal()
+ {
+ showDeleteModal = false;
+ }
+
+ private async Task DeleteInvoice()
+ {
+ if (selectedInvoice != null)
+ {
+ try
+ {
+ NavigationManager.NavigateTo($"/donations/deletedonationinvoice?id={selectedInvoice.Id}");
+ showDeleteModal = false;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting invoice: {ex.Message}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Financial/AddJournalEntry.razor b/src/AccountGoWeb/Components/Pages/Financial/AddJournalEntry.razor
new file mode 100644
index 000000000..d97f2d52e
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Financial/AddJournalEntry.razor
@@ -0,0 +1,347 @@
+@using System.Net.Http.Json
+@using Dto.Financial
+@inject IHttpClientFactory HttpFactory
+@inject IConfiguration Config
+@using JournalEntryDto = Dto.Financial.JournalEntry
+@using JournalEntryLineDto = Dto.Financial.JournalEntryLine
+
+
+@* Add Journal Entry *@
+
+@if (!string.IsNullOrEmpty(SuccessMessage))
+{
+ @SuccessMessage
+}
+
+@if (!string.IsNullOrEmpty(ErrorMessage))
+{
+ @ErrorMessage
+}
+
+
+
+
+
+
+
+
+
Header
+
+
+ Date
+
+
+
+
+ Reference No
+
+
+
+
+ Voucher Type
+
+ -- select type --
+ Opening Balances
+ Closing Entries
+ Adjustment Entries
+ Correction Entries
+ Transfer Entries
+
+
+
+
+ Memo
+
+
+
+
+
+
+
+
+ Lines
+
+ Add Line
+
+
+
+
+ @if (Accounts == null || Accounts.Count == 0)
+ {
+
+ Accounts are not loaded yet. You won’t be able to save until accounts are available.
+
+ }
+
+
+
+
+ Account
+ Dr/Cr
+ Amount
+ Memo
+
+
+
+
+ @if (Entry.JournalEntryLines != null)
+ {
+ @foreach (var line in Entry.JournalEntryLines)
+ {
+ var currentLine = line;
+
+
+ @if (Accounts != null && Accounts.Count > 0)
+ {
+
+ -- select account --
+ @foreach (var acct in Accounts)
+ {
+
+ @acct.AccountCode - @acct.AccountName
+
+ }
+
+ }
+ else
+ {
+ No accounts
+ }
+
+
+
+
+ Debit
+ Credit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RemoveLine(currentLine)">
+
+
+
+
+ }
+ }
+
+
+
+ Total Debit
+ @TotalDebit.ToString("0.00")
+
+
+
+ Total Credit
+ @TotalCredit.ToString("0.00")
+
+
+
+
+
+
+
+
+
+
+ @if (IsSaving)
+ {
+ Saving...
+ }
+ else
+ {
+ Save
+ }
+
+
+
+
+
+@code {
+ private JournalEntryDto Entry = new();
+
+ private bool IsSaving;
+ private string? ErrorMessage;
+ private string? SuccessMessage;
+
+ private List AccountTree = new();
+ private List Accounts = new();
+
+ private decimal TotalDebit =>
+ Entry.JournalEntryLines?
+ .Where(l => l.DrCr == 1) // 1 = Debit
+ .Sum(l => l.Amount ?? 0) ?? 0;
+
+ private decimal TotalCredit =>
+ Entry.JournalEntryLines?
+ .Where(l => l.DrCr == 2) // 2 = Credit
+ .Sum(l => l.Amount ?? 0) ?? 0;
+
+ protected override async Task OnInitializedAsync()
+ {
+ // remove this if BaseDto doesn't have Id
+ // Entry.Id = 0;
+
+ Entry.JournalDate = DateTime.Today;
+ Entry.Posted = false;
+ Entry.VoucherType ??= 1;
+ Entry.JournalEntryLines ??= new List();
+
+ AddLine();
+ await LoadAccountsAsync();
+ }
+
+ private async Task LoadAccountsAsync()
+ {
+ try
+ {
+ var apiBaseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(apiBaseUrl))
+ {
+ ErrorMessage = "ApiUrl is not configured.";
+ return;
+ }
+
+ var client = HttpFactory.CreateClient();
+ var url = $"{apiBaseUrl}financials/accounts";
+
+ var tree = await client.GetFromJsonAsync>(url);
+
+ if (tree != null)
+ {
+ AccountTree = tree;
+ Accounts = FlattenAccounts(AccountTree)
+ .OrderBy(a => a.AccountCode)
+ .ToList();
+ }
+ else
+ {
+ ErrorMessage = "Could not load accounts (empty response).";
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"Error loading accounts: {ex.Message}";
+ }
+ }
+
+ private List FlattenAccounts(IEnumerable nodes)
+ {
+ var list = new List();
+
+ void Walk(IEnumerable items)
+ {
+ foreach (var a in items)
+ {
+ list.Add(a);
+ if (a.ChildAccounts != null && a.ChildAccounts.Count > 0)
+ Walk(a.ChildAccounts);
+ }
+ }
+
+ Walk(nodes);
+ return list;
+ }
+
+ private void AddLine()
+ {
+ Entry.JournalEntryLines!.Add(new JournalEntryLineDto
+ {
+
+ DrCr = 1,
+ Amount = 0
+ });
+ }
+
+ private void RemoveLine(JournalEntryLineDto line)
+ {
+ Entry.JournalEntryLines!.Remove(line);
+ }
+
+ private async Task HandleValidSubmit()
+ {
+ ErrorMessage = null;
+ SuccessMessage = null;
+ IsSaving = true;
+
+ @* if (TotalDebit != TotalCredit)
+ {
+ ErrorMessage = "Debits and credits are not equal.";
+ IsSaving = false;
+ return;
+ } *@
+
+ if (Entry.JournalEntryLines == null || Entry.JournalEntryLines.Any(l => l.AccountId == null))
+ {
+ ErrorMessage = "All lines must have an account selected.";
+ IsSaving = false;
+ return;
+ }
+
+ try
+ {
+ var apiBaseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(apiBaseUrl))
+ {
+ ErrorMessage = "ApiUrl is not configured.";
+ return;
+ }
+
+ var client = HttpFactory.CreateClient();
+ var url = $"{apiBaseUrl}financials/SaveJournalEntry";
+
+ var response = await client.PostAsJsonAsync(url, Entry);
+
+ if (response.IsSuccessStatusCode)
+ {
+ SuccessMessage = "Journal entry saved successfully.";
+
+ // reset using DTO alias
+ Entry = new JournalEntryDto
+ {
+ JournalDate = DateTime.Today,
+ VoucherType = 1,
+ Posted = false,
+ JournalEntryLines = new List()
+ };
+ AddLine();
+ }
+ else
+ {
+ try
+ {
+ var errors = await response.Content.ReadFromJsonAsync();
+ ErrorMessage = errors != null && errors.Length > 0
+ ? string.Join("; ", errors)
+ : $"Error saving (HTTP {(int)response.StatusCode})";
+ }
+ catch
+ {
+ ErrorMessage = $"Error saving (HTTP {(int)response.StatusCode})";
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"Unexpected error: {ex.Message}";
+ }
+ finally
+ {
+ IsSaving = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Financial/ChartOfAccounts.razor b/src/AccountGoWeb/Components/Pages/Financial/ChartOfAccounts.razor
new file mode 100644
index 000000000..e34dbe7cd
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Financial/ChartOfAccounts.razor
@@ -0,0 +1,638 @@
+@page "/financials/chart-of-accounts"
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@using System.Net.Http.Json
+@using LibraryGDB.Models.Financial
+@using Microsoft.JSInterop
+@using Microsoft.Net.Http.Headers
+@using Microsoft.AspNetCore.Components
+@inject IHttpClientFactory ClientFactory
+@inject Microsoft.JSInterop.IJSRuntime JSRuntime
+
+Chart of Accounts
+
+@if (getError || accounts is null)
+{
+ Unable to get data. Please try again later.
+}
+else if (isLoading)
+{
+ Loading accounts...
+}
+else
+{
+ @*
+ @foreach (var item in accounts)
+ {
+ @item.AccountName
+ }
+ *@
+
+
+
OpenAddModal()">Add Account
+
+
+
+
+ Code
+ Name
+ Balance
+ Debit
+ Credit
+ Actions
+
+
+
+ @for (int accountIdx = 0; accountIdx < accounts.Count(); ++accountIdx)
+ {
+ var account = accounts.ToList()[accountIdx];
+ var accountTargetId = $"asset-{accountIdx}";
+
+
+ @account.AccountCode
+ @account.AccountName
+ @account.TotalBalance
+ @account.TotalDebitBalance
+ @account.TotalCreditBalance
+
+ OpenAddModal(account)" @onclick:stopPropagation="true">Add Account
+ OpenEditModal(account)" @onclick:stopPropagation="true">Edit
+ OpenDeleteModal(account)" @onclick:stopPropagation="true">Delete
+
+
+
+
+
+
+
+
+
+
+ Code
+ Name
+ Balance
+ Debit
+ Credit
+ Actions
+
+
+
+ @for (int childAccountIdx = 0; childAccountIdx < account.ChildAccounts!.Count; ++childAccountIdx)
+ {
+ var childAccount = account.ChildAccounts.ToList()[childAccountIdx];
+ var childAccountTargetId = $"asset-{accountIdx}-{childAccountIdx}";
+
+
+ @childAccount.AccountCode
+ @childAccount.AccountName
+ @childAccount.TotalBalance
+ @childAccount.TotalDebitBalance
+ @childAccount.TotalCreditBalance
+
+ OpenAddModal(childAccount)" @onclick:stopPropagation="true">Add Account
+ OpenEditModal(childAccount)" @onclick:stopPropagation="true">Edit
+ OpenDeleteModal(childAccount)" @onclick:stopPropagation="true">Delete
+
+
+
+
+
+
+
+
+
+
+ Code
+ Name
+ Balance
+ Debit
+ Credit
+ Actions
+
+
+
+ @for (int grandChildAccountIdx = 0; grandChildAccountIdx < childAccount.ChildAccounts!.Count; ++grandChildAccountIdx)
+ {
+ var grandChildAccount = childAccount.ChildAccounts.ToList()[grandChildAccountIdx];
+ var grandChildAccountTargetId = $"asset-{accountIdx}-{childAccountIdx}-{grandChildAccountIdx}";
+
+
+ @grandChildAccount.AccountCode
+ @grandChildAccount.AccountName
+ @grandChildAccount.TotalBalance
+ @grandChildAccount.TotalDebitBalance
+ @grandChildAccount.TotalCreditBalance
+
+ OpenAddModal(grandChildAccount)" @onclick:stopPropagation="true">Add Account
+ OpenEditModal(grandChildAccount)" @onclick:stopPropagation="true">Edit
+ OpenDeleteModal(grandChildAccount)" @onclick:stopPropagation="true">Delete
+
+
+
+
+
+
+
+
+
+
+ Code
+ Name
+ Balance
+ Debit
+ Credit
+ Actions
+
+
+
+ @for (int greatGrandChildAccountIdx = 0; greatGrandChildAccountIdx < grandChildAccount.ChildAccounts!.Count; ++greatGrandChildAccountIdx)
+ {
+ var greatGrandChildAccount = grandChildAccount.ChildAccounts.ToList()[greatGrandChildAccountIdx];
+ var greatGrandChildAccountTargetId = $"asset-{accountIdx}-{childAccountIdx}-{grandChildAccountIdx}-{greatGrandChildAccountIdx}";
+
+
+ @greatGrandChildAccount.AccountCode
+ @greatGrandChildAccount.AccountName
+ @greatGrandChildAccount.TotalBalance
+ @greatGrandChildAccount.TotalDebitBalance
+ @greatGrandChildAccount.TotalCreditBalance
+
+ OpenAddModal(greatGrandChildAccount)" @onclick:stopPropagation="true">Add Account
+ OpenEditModal(greatGrandChildAccount)" @onclick:stopPropagation="true">Edit
+
+
+
+ @if (greatGrandChildAccount.ChildAccounts != null && greatGrandChildAccount.ChildAccounts.Count > 0)
+ {
+
+
+
+ @RenderNestedAccounts(greatGrandChildAccount.ChildAccounts, $"{accountIdx}-{childAccountIdx}-{grandChildAccountIdx}-{greatGrandChildAccountIdx}")
+
+
+
+ }
+ }
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+ }
+
+
+
+
+}
+@if (isAddModalVisible || isEditModalVisible)
+{
+
+
+
+
+
+
+ Account Code
+ selectedAccount!.AccountCode = e.Value?.ToString() ?? string.Empty"
+ disabled="@isEditModalVisible" />
+
+
+ Account Name
+ selectedAccount!.AccountName = e.Value?.ToString() ?? string.Empty" />
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
@errorMessage
+ }
+
+
+
+
+
+}
+
+@if (isDeleteModalVisible)
+{
+
+
+
+
+
+
+ Are you sure you want to delete the account
+ @selectedAccount?.AccountName
+ with code @selectedAccount?.AccountCode ?
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
@errorMessage
+ }
+
+
+
+
+
+}
+
+@code {
+ private List accounts = new();
+ private AccountViewModel? selectedAccount = null;
+ private AccountViewModel? parentAccount = null; // Track parent account when adding sub-account
+ private bool isAddModalVisible = false;
+ private bool isEditModalVisible = false;
+ private bool isDeleteModalVisible = false;
+ private string errorMessage = string.Empty;
+ private bool isLoading = true;
+ private bool getError = false;
+
+ // Recursive method to render nested accounts at any depth
+ private RenderFragment RenderNestedAccounts(List nestedAccounts, string parentPath)
+ {
+ return builder =>
+ {
+ int sequence = 0;
+ builder.OpenElement(sequence++, "table");
+ builder.AddAttribute(sequence++, "class", "table table-striped");
+
+ // Table header
+ builder.OpenElement(sequence++, "thead");
+ builder.OpenElement(sequence++, "tr");
+ builder.AddMarkupContent(sequence++, "Code Name Balance Debit Credit Actions ");
+ builder.CloseElement(); // tr
+ builder.CloseElement(); // thead
+
+ // Table body
+ builder.OpenElement(sequence++, "tbody");
+
+ for (int idx = 0; idx < nestedAccounts.Count; idx++)
+ {
+ var account = nestedAccounts[idx];
+ var targetId = $"asset-{parentPath}-{idx}";
+
+ // Account row
+ builder.OpenElement(sequence++, "tr");
+ builder.AddAttribute(sequence++, "data-bs-toggle", "collapse");
+ builder.AddAttribute(sequence++, "data-bs-target", $"#{targetId}");
+ builder.AddAttribute(sequence++, "aria-expanded", "false");
+ builder.AddAttribute(sequence++, "aria-controls", targetId);
+
+ builder.OpenElement(sequence++, "td");
+ builder.AddContent(sequence++, account.AccountCode);
+ builder.CloseElement();
+
+ builder.OpenElement(sequence++, "td");
+ builder.AddContent(sequence++, account.AccountName);
+ builder.CloseElement();
+
+ builder.OpenElement(sequence++, "td");
+ builder.AddContent(sequence++, account.TotalBalance);
+ builder.CloseElement();
+
+ builder.OpenElement(sequence++, "td");
+ builder.AddContent(sequence++, account.TotalDebitBalance);
+ builder.CloseElement();
+
+ builder.OpenElement(sequence++, "td");
+ builder.AddContent(sequence++, account.TotalCreditBalance);
+ builder.CloseElement();
+
+ builder.OpenElement(sequence++, "td");
+ builder.AddAttribute(sequence++, "onclick:stopPropagation", "true");
+ builder.OpenElement(sequence++, "button");
+ builder.AddAttribute(sequence++, "class", "btn btn-success btn-sm");
+ builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => OpenAddModal(account)));
+ builder.AddAttribute(sequence++, "onclick:stopPropagation", "true");
+ builder.AddContent(sequence++, "Add Account");
+ builder.CloseElement(); // button
+ builder.OpenElement(sequence++, "button");
+ builder.AddAttribute(sequence++, "class", "btn btn-primary btn-sm");
+ builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => OpenEditModal(account)));
+ builder.AddAttribute(sequence++, "onclick:stopPropagation", "true");
+ builder.AddContent(sequence++, "Edit");
+ builder.CloseElement(); // button
+ builder.OpenElement(sequence++, "button");
+ builder.AddAttribute(sequence++, "class", "btn btn-danger btn-sm");
+ builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => OpenDeleteModal(account)));
+ builder.AddAttribute(sequence++, "onclick:stopPropagation", "true");
+ builder.AddContent(sequence++, "Delete");
+ builder.CloseElement(); // button
+ builder.CloseElement(); // td
+
+ builder.CloseElement(); // tr
+
+ // Collapsible row for children
+ if (account.ChildAccounts != null && account.ChildAccounts.Count > 0)
+ {
+ builder.OpenElement(sequence++, "tr");
+ builder.OpenElement(sequence++, "td");
+ builder.AddAttribute(sequence++, "colspan", "6");
+
+ builder.OpenElement(sequence++, "div");
+ builder.AddAttribute(sequence++, "class", "collapse");
+ builder.AddAttribute(sequence++, "id", targetId);
+ builder.AddAttribute(sequence++, "aria-expanded", "false");
+ builder.AddAttribute(sequence++, "aria-controls", targetId);
+
+ // Recursively render children
+ builder.AddContent(sequence++, RenderNestedAccounts(account.ChildAccounts, $"{parentPath}-{idx}"));
+
+ builder.CloseElement(); // div
+ builder.CloseElement(); // td
+ builder.CloseElement(); // tr
+ }
+ }
+
+ builder.CloseElement(); // tbody
+ builder.CloseElement(); // table
+ };
+ }
+
+ // Fetch accounts from API on initialization
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadAccountsFromApi();
+ isLoading = false;
+ }
+
+ // Load accounts from API
+ private async Task LoadAccountsFromApi()
+ {
+ try
+ {
+ string apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+
+ var response = await client.GetAsync($"{apiUrl}financials/accounts");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var jsonString = await response.Content.ReadAsStringAsync();
+ accounts = JsonSerializer.Deserialize>(jsonString, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ }) ?? new List();
+ }
+ else
+ {
+ getError = true;
+ }
+ }
+ catch
+ {
+ getError = true;
+ }
+
+ // Notify Blazor that the state has changed and UI needs to update
+ StateHasChanged();
+ }
+
+
+ // Open Add Modal
+ private void OpenAddModal(AccountViewModel? parent = null)
+ {
+ parentAccount = parent;
+ selectedAccount = new AccountViewModel();
+ errorMessage = string.Empty;
+ isAddModalVisible = true;
+ }
+
+ // Open Edit Modal
+ private void OpenEditModal(AccountViewModel account)
+ {
+ selectedAccount = new AccountViewModel
+ {
+ AccountCode = account.AccountCode,
+ AccountName = account.AccountName,
+ TotalBalance = account.TotalBalance,
+ TotalDebitBalance = account.TotalDebitBalance,
+ TotalCreditBalance = account.TotalCreditBalance,
+ ChildAccounts = account.ChildAccounts
+ };
+ errorMessage = string.Empty;
+ isEditModalVisible = true;
+ }
+
+ // Close Add or Edit Modal
+ private void CloseModal()
+ {
+ isAddModalVisible = false;
+ isEditModalVisible = false;
+ selectedAccount = null;
+ parentAccount = null;
+ errorMessage = string.Empty;
+ }
+
+ // Add or Update Account
+ private async Task SaveAccount()
+ {
+ if (selectedAccount == null)
+ {
+ errorMessage = "No account selected.";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(selectedAccount.AccountCode) || string.IsNullOrWhiteSpace(selectedAccount.AccountName))
+ {
+ errorMessage = "Both Account Code and Account Name are required.";
+ return;
+ }
+
+ try
+ {
+ string apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+
+ var accountDto = new
+ {
+ AccountCode = selectedAccount.AccountCode,
+ AccountName = selectedAccount.AccountName,
+ ParentAccountId = parentAccount?.Id // Include parent account ID if adding a sub-account
+ };
+
+ HttpResponseMessage response;
+
+ if (isEditModalVisible)
+ {
+ // Update existing account via API
+ response = await client.PutAsJsonAsync(
+ $"{apiUrl}financials/UpdateAccount/{selectedAccount.AccountCode}",
+ accountDto);
+ }
+ else
+ {
+ // Add new account via API
+ response = await client.PostAsJsonAsync(
+ $"{apiUrl}financials/AddAccount",
+ accountDto);
+ }
+
+ if (response.IsSuccessStatusCode)
+ {
+ errorMessage = string.Empty;
+ // Close modal first to ensure UI state is updated
+ CloseModal();
+ // Reload accounts from API to get updated data
+ await LoadAccountsFromApi();
+ // StateHasChanged is called in LoadAccountsFromApi, but we also call it here
+ // to ensure the modal closes immediately
+ StateHasChanged();
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+
+ // Try to parse JSON error response for better error messages
+ string detailedError = errorContent;
+ try
+ {
+ // If the response is JSON, try to extract meaningful error messages
+ if (errorContent.Trim().StartsWith("{") || errorContent.Trim().StartsWith("["))
+ {
+ var errorObj = JsonSerializer.Deserialize(errorContent);
+ if (errorObj.TryGetProperty("errors", out var errors))
+ {
+ var errorList = new List();
+ foreach (var error in errors.EnumerateObject())
+ {
+ foreach (var msg in error.Value.EnumerateArray())
+ {
+ errorList.Add($"{error.Name}: {msg.GetString()}");
+ }
+ }
+ detailedError = string.Join("; ", errorList);
+ }
+ else if (errorObj.TryGetProperty("message", out var message))
+ {
+ detailedError = message.GetString() ?? errorContent;
+ }
+ }
+ }
+ catch
+ {
+ // If parsing fails, use the raw error content
+ }
+
+ errorMessage = $"Failed to save account ({response.StatusCode}): {detailedError}";
+ StateHasChanged(); // Update UI to show error message
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving account: {ex.Message}";
+ if (ex.InnerException != null)
+ {
+ errorMessage += $" ({ex.InnerException.Message})";
+ }
+ StateHasChanged(); // Update UI to show error message
+ }
+ }
+
+ // Open Delete Modal
+ private void OpenDeleteModal(AccountViewModel account)
+ {
+ selectedAccount = account;
+ isDeleteModalVisible = true;
+ errorMessage = string.Empty;
+ }
+
+ // Close Delete Modal
+ private void CloseDeleteModal()
+ {
+ isDeleteModalVisible = false;
+ selectedAccount = null;
+ errorMessage = string.Empty;
+ }
+
+ // Delete Account
+ private async Task ConfirmDeleteAccount()
+ {
+ if (selectedAccount == null)
+ {
+ errorMessage = "No account selected.";
+ StateHasChanged();
+ return;
+ }
+
+ try
+ {
+ string apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+
+ var response = await client.DeleteAsync(
+ $"{apiUrl}financials/DeleteAccount/{selectedAccount.AccountCode}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ errorMessage = string.Empty;
+ // Close modal first to ensure UI state is updated
+ CloseDeleteModal();
+ // Reload accounts from API to reflect deletion
+ await LoadAccountsFromApi();
+ // StateHasChanged is called in LoadAccountsFromApi, but we also call it here
+ // to ensure the modal closes immediately
+ StateHasChanged();
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to delete account: {response.StatusCode}. {errorContent}";
+ StateHasChanged(); // Update UI to show error message
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error deleting account: {ex.Message}";
+ StateHasChanged(); // Update UI to show error message
+ }
+ }
+
+ // ViewModel for Accounts
+ public class AccountViewModel
+ {
+ public int Id { get; set; }
+ public int? ParentAccountId { get; set; }
+ public string AccountCode { get; set; } = string.Empty;
+ public string AccountName { get; set; } = string.Empty;
+ public decimal TotalBalance { get; set; }
+ public decimal TotalDebitBalance { get; set; }
+ public decimal TotalCreditBalance { get; set; }
+ public List ChildAccounts { get; set; } = new();
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Financial/JournalEntries.razor b/src/AccountGoWeb/Components/Pages/Financial/JournalEntries.razor
new file mode 100644
index 000000000..7642a051f
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Financial/JournalEntries.razor
@@ -0,0 +1,127 @@
+@namespace AccountGoWeb.Components.Pages.Financial
+@using JournalEntryDto = Dto.Financial.JournalEntry
+@using JournalEntryLineDto = Dto.Financial.JournalEntryLine
+@using System.Net.Http.Json
+
+@inject HttpClient Http
+@inject IConfiguration Config
+
+
+
+
+
+ @if (IsLoading && ErrorMessage is null)
+ {
+
Loading journal entries...
+ }
+ else if (ErrorMessage is not null)
+ {
+
Error: @ErrorMessage
+ }
+ else if (Entries is null || !Entries.Any())
+ {
+
+ No journal entries found.
+
+ }
+ else
+ {
+
+
+ }
+
+
+
+
+@code {
+ // 🔴 CHANGE THESE TYPES TO THE DTO ALIAS
+ private List? Entries;
+ private string? ErrorMessage;
+ private bool IsLoading = true;
+
+ private JournalEntryDto? SelectedEntry;
+
+ private string ViewLinkHref => SelectedEntry is null
+ ? "/Financials/JournalEntry"
+ : $"/Financials/JournalEntry?id={SelectedEntry.Id}";
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ var baseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(baseUrl))
+ {
+ ErrorMessage = "ApiUrl is not configured.";
+ return;
+ }
+
+ var url = $"{baseUrl}financials/journalentries";
+
+ Entries = await Http.GetFromJsonAsync>(url);
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = ex.Message;
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ private void OnRowClick(JournalEntryDto entry)
+ {
+ SelectedEntry = entry;
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Financial/JournalEntry.razor b/src/AccountGoWeb/Components/Pages/Financial/JournalEntry.razor
new file mode 100644
index 000000000..5514438b3
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Financial/JournalEntry.razor
@@ -0,0 +1,391 @@
+@using System.Net.Http.Json
+@using Dto.Financial
+@inject IHttpClientFactory HttpFactory
+@inject IConfiguration Config
+@using JournalEntryDto = Dto.Financial.JournalEntry
+@using JournalEntryLineDto = Dto.Financial.JournalEntryLine
+
+
+
+@if (IsLoading)
+{
+ Loading...
+}
+else if (!string.IsNullOrEmpty(ErrorMessage))
+{
+ @ErrorMessage
+}
+else if (Entry is null)
+{
+ Journal entry not found.
+}
+else
+{
+ @if (!string.IsNullOrEmpty(SuccessMessage))
+ {
+ @SuccessMessage
+ }
+
+
+
+
+
+
Header
+
+
+
Date
+
@Entry.JournalDate.ToString("yyyy-MM-dd")
+
+
+
+
Reference No
+
@Entry.ReferenceNo
+
+
+
+
Voucher Type
+
@Entry.VoucherType
+
+
+
+
+
+ Status
+ @if (Entry.Posted.GetValueOrDefault())
+ {
+ Posted
+ }
+ else
+ {
+ Not Posted
+ }
+
+
+
+
+
+
Lines
+
+
+ @if (Entry.JournalEntryLines == null || Entry.JournalEntryLines.Count == 0)
+ {
+
No lines found for this journal entry.
+ }
+ else
+ {
+
+
+
+ }
+
+
+
+
+ @if (!Entry.Posted.GetValueOrDefault())
+ {
+
+
+ @if (IsSaving)
+ {
+ Saving...
+ }
+ else
+ {
+ Save
+ }
+
+
+
+ @if (IsPosting)
+ {
+ Posting...
+ }
+ else if (!Entry.ReadyForPosting.GetValueOrDefault())
+ {
+ Not Ready for Posting
+ }
+ else
+ {
+ Post
+ }
+
+ }
+
+
+}
+
+@code {
+ [Parameter] public int Id { get; set; }
+
+ private JournalEntryDto? Entry;
+ private bool IsLoading = true;
+ private bool IsPosting = false;
+ private bool IsSaving = false;
+ private string? ErrorMessage;
+ private string? SuccessMessage;
+ private List Accounts = new();
+ private decimal TotalDebit =>
+ Entry?.JournalEntryLines?
+ .Where(l => l.DrCr == 1)
+ .Sum(l => l.Amount ?? 0) ?? 0;
+
+ private decimal TotalCredit =>
+ Entry?.JournalEntryLines?
+ .Where(l => l.DrCr == 2)
+ .Sum(l => l.Amount ?? 0) ?? 0;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadAccountsAsync();
+ await LoadEntryAsync();
+ }
+
+ private async Task LoadAccountsAsync()
+ {
+ try
+ {
+ var apiBaseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(apiBaseUrl))
+ {
+ return;
+ }
+
+ var client = HttpFactory.CreateClient();
+ var url = $"{apiBaseUrl}financials/Accounts";
+
+ var result = await client.GetFromJsonAsync>(url);
+
+ // result is a tree (each Account may have ChildAccounts)
+ var flatList = new List();
+ if (result != null)
+ {
+ FlattenAccounts(result, flatList);
+ }
+
+ Accounts = flatList;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error loading accounts: {ex.Message}");
+ }
+ }
+
+ private void FlattenAccounts(IEnumerable source, List destination)
+ {
+ foreach (var acc in source)
+ {
+ destination.Add(acc);
+
+ if (acc.ChildAccounts != null && acc.ChildAccounts.Count > 0)
+ {
+ FlattenAccounts(acc.ChildAccounts, destination);
+ }
+ }
+ }
+
+ private async Task LoadEntryAsync()
+ {
+ try
+ {
+ ErrorMessage = null;
+ IsLoading = true;
+
+ var apiBaseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(apiBaseUrl))
+ {
+ ErrorMessage = "ApiUrl is not configured.";
+ return;
+ }
+
+ var client = HttpFactory.CreateClient();
+ var url = $"{apiBaseUrl}financials/JournalEntry?id={Id}";
+
+ Entry = await client.GetFromJsonAsync(url);
+
+ if (Entry == null)
+ ErrorMessage = "Journal entry not found.";
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"Error loading journal entry: {ex.Message}";
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ private async Task PostEntry()
+ {
+ if (Entry == null)
+ return;
+
+ ErrorMessage = null;
+ SuccessMessage = null;
+ IsPosting = true;
+
+ try
+ {
+ var apiBaseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(apiBaseUrl))
+ {
+ ErrorMessage = "ApiUrl is not configured.";
+ return;
+ }
+
+ var client = HttpFactory.CreateClient();
+ var url = $"{apiBaseUrl}financials/PostJournalEntry";
+
+ var response = await client.PostAsJsonAsync(url, Entry);
+
+ if (response.IsSuccessStatusCode)
+ {
+ SuccessMessage = "Journal entry posted successfully.";
+ await LoadEntryAsync(); // refresh Posted/ReadyForPosting
+ }
+ else
+ {
+ var errors = await response.Content.ReadFromJsonAsync();
+ ErrorMessage = errors != null && errors.Length > 0
+ ? string.Join("; ", errors)
+ : $"Error posting (HTTP {(int)response.StatusCode})";
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"Unexpected error while posting: {ex.Message}";
+ }
+ finally
+ {
+ IsPosting = false;
+ }
+ }
+
+
+ private async Task SaveEntry()
+ {
+ if (Entry == null)
+ return;
+
+ ErrorMessage = null;
+ SuccessMessage = null;
+ IsSaving = true;
+
+ try
+ {
+ var apiBaseUrl = Config["ApiUrl"];
+ if (string.IsNullOrWhiteSpace(apiBaseUrl))
+ {
+ ErrorMessage = "ApiUrl is not configured.";
+ return;
+ }
+
+ var client = HttpFactory.CreateClient();
+ var url = $"{apiBaseUrl}financials/SaveJournalEntry";
+
+ var response = await client.PostAsJsonAsync(url, Entry);
+
+ if (response.IsSuccessStatusCode)
+ {
+ SuccessMessage = "Journal entry saved successfully.";
+ await LoadEntryAsync(); // refresh from DB (lines, totals, ReadyForPosting)
+ }
+ else
+ {
+ var errors = await response.Content.ReadFromJsonAsync();
+ ErrorMessage = errors != null && errors.Length > 0
+ ? string.Join("; ", errors)
+ : $"Error saving (HTTP {(int)response.StatusCode})";
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"Unexpected error while saving: {ex.Message}";
+ }
+ finally
+ {
+ IsSaving = false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Inventory/ICJ.razor b/src/AccountGoWeb/Components/Pages/Inventory/ICJ.razor
new file mode 100644
index 000000000..4c231bb97
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Inventory/ICJ.razor
@@ -0,0 +1,126 @@
+@* ICJ.razor - fetches its own data *@
+@using Dto.Inventory
+@using Microsoft.Extensions.Configuration
+@inject IHttpClientFactory HttpClientFactory
+@inject IConfiguration Configuration
+
+@if (isLoading)
+{
+
+}
+else if (hasError)
+{
+ Error loading inventory data.
+}
+else
+{
+
+
+
+ Id
+ Item
+ Measurement
+ IN
+ OUT
+ Date
+
+
+
+ @if (InventoryData == null || InventoryData.Count == 0)
+ {
+
+ No Rows to Show
+
+ }
+ else
+ {
+ @foreach (var row in InventoryData)
+ {
+ OnRowClicked(row)" class="icjRow">
+ @row.Id
+ @row.Item
+ @row.Measurement
+ @row.In
+ @row.Out
+ @row.Date.ToShortDateString()
+
+ }
+ }
+
+
+}
+
+
+
+@code {
+ private List InventoryData { get; set; } = new();
+ private bool isLoading = true;
+ private bool hasError = false;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ var httpClient = HttpClientFactory.CreateClient();
+ httpClient.BaseAddress = new Uri(Configuration["ApiUrl"] ?? "https://gdbapi.azurewebsites.net/api/");
+ httpClient.DefaultRequestHeaders.Accept.Clear();
+ httpClient.DefaultRequestHeaders.Accept.Add(
+ new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
+
+ var response = await httpClient.GetAsync("Inventory/ICJ");
+ if (response.IsSuccessStatusCode)
+ {
+ InventoryData = await response.Content.ReadFromJsonAsync>() ?? new List();
+ }
+ else
+ {
+ hasError = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Error loading ICJ data: " + ex.Message);
+ hasError = true;
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void OnRowClicked(InventoryControlJournal row)
+ {
+ Console.WriteLine($"Row clicked: {row.Id} - {row.Item}");
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Inventory/ItemForm.razor b/src/AccountGoWeb/Components/Pages/Inventory/ItemForm.razor
new file mode 100644
index 000000000..e0de4036e
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Inventory/ItemForm.razor
@@ -0,0 +1,413 @@
+@using Dto.Inventory
+@using Microsoft.AspNetCore.Components.Forms
+@inject IHttpClientFactory HttpClientFactory
+@inject NavigationManager Navigation
+
+
+
+ Edit
+
+
+
+
+
+
+
+ @* General Section *@
+
+
+
+
+
+
+
Smallest UOM
+
+
+ Select...
+ @foreach (var measurement in measurements)
+ {
+ @measurement.Text
+ }
+
+
+
+
+
Category
+
+
+ Select...
+ @foreach (var category in itemCategories)
+ {
+ @category.Text
+ }
+
+
+
+
+
Item Tax Group
+
+
+ Select...
+ @foreach (var taxGroup in itemTaxGroups)
+ {
+ @taxGroup.Text
+ }
+
+
+
+
+
+
+
+ @* Pricing Section *@
+
+
+
+
+
+
Sell Description
+
+
+
+
+
+
+
Sell UOM
+
+
+ Select...
+ @foreach (var measurement in measurements)
+ {
+ @measurement.Text
+ }
+
+
+
+
+
+
+
Purchase Description
+
+
+
+
+
+
+
Purchase UOM
+
+
+ Select...
+ @foreach (var measurement in measurements)
+ {
+ @measurement.Text
+ }
+
+
+
+
+
+
+
+ @* Invoicing Section *@
+
+
+
+
+
+
Sales Account
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+
+
Adjustment
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
Inventory Account
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
Cost of Good Sold
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+
+
+
+
+
+ @if (isSaving)
+ {
+
+ }
+ Save
+
+
Close
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
@errorMessage
+ }
+ @if (!string.IsNullOrEmpty(successMessage))
+ {
+
@successMessage
+ }
+
+
+
+@code {
+ [Parameter]
+ public int ItemId { get; set; }
+
+ private Item item = new Item();
+ private bool isEditMode = false;
+ private bool isSaving = false;
+ private string? errorMessage;
+ private string? successMessage;
+
+ private List accounts = new();
+ private List measurements = new();
+ private List itemCategories = new();
+ private List itemTaxGroups = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadDropdownData();
+
+ if (ItemId == 0)
+ {
+ // New item
+ isEditMode = true;
+ item.No = new Random().Next(1, 99999).ToString();
+ }
+ else
+ {
+ // Edit existing item
+ await LoadItem();
+ }
+ }
+
+ private async Task LoadItem()
+ {
+ try
+ {
+ var http = HttpClientFactory.CreateClient();
+ var apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var response = await http.GetAsync($"{apiUrl}inventory/item?id={ItemId}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ item = await response.Content.ReadFromJsonAsync- () ?? new Item();
+ }
+ else
+ {
+ errorMessage = "Failed to load item data.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading item: {ex.Message}";
+ }
+ }
+
+ private async Task LoadDropdownData()
+ {
+ try
+ {
+ var http = HttpClientFactory.CreateClient();
+ var apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ // Load accounts
+ var accountsResponse = await http.GetAsync($"{apiUrl}financials/accounts");
+ if (accountsResponse.IsSuccessStatusCode)
+ {
+ var accountsList = await accountsResponse.Content.ReadFromJsonAsync
>() ?? new();
+ accounts = accountsList.Select(a => new SelectListItem { Value = a.Id.ToString(), Text = a.AccountName }).ToList();
+ }
+
+ // Load measurements
+ var measurementsResponse = await http.GetAsync($"{apiUrl}common/measurements");
+ if (measurementsResponse.IsSuccessStatusCode)
+ {
+ var measurementsList = await measurementsResponse.Content.ReadFromJsonAsync>() ?? new();
+ measurements = measurementsList.Select(m => new SelectListItem
+ {
+ Value = m.Id.ToString(),
+ Text = m.Description
+ }).ToList();
+ }
+
+ // Load item categories
+ var categoriesResponse = await http.GetAsync($"{apiUrl}common/itemcategories");
+ if (categoriesResponse.IsSuccessStatusCode)
+ {
+ var categoriesList = await categoriesResponse.Content.ReadFromJsonAsync>() ?? new();
+ itemCategories = categoriesList.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Name }).ToList();
+ }
+
+ // Load item tax groups
+ var taxGroupsResponse = await http.GetAsync($"{apiUrl}tax/itemtaxgroups");
+ if (taxGroupsResponse.IsSuccessStatusCode)
+ {
+ var taxGroupsList = await taxGroupsResponse.Content.ReadFromJsonAsync>() ?? new();
+ itemTaxGroups = taxGroupsList.Select(t => new SelectListItem { Value = t.Id.ToString(), Text = t.Name }).ToList();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading dropdown data: {ex.Message}";
+ }
+ }
+
+ private void ToggleEdit()
+ {
+ isEditMode = !isEditMode;
+ }
+
+ private async Task HandleValidSubmit()
+ {
+ isSaving = true;
+ errorMessage = null;
+ successMessage = null;
+
+ try
+ {
+ var http = HttpClientFactory.CreateClient();
+ var apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var response = await http.PostAsJsonAsync($"{apiUrl}inventory/saveitem", item);
+
+ if (response.IsSuccessStatusCode)
+ {
+ successMessage = "Item saved successfully!";
+ await Task.Delay(1000);
+ Navigation.NavigateTo("/Inventory");
+ }
+ else
+ {
+ errorMessage = $"Failed to save item. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving item: {ex.Message}";
+ }
+ finally
+ {
+ isSaving = false;
+ }
+ }
+
+ private class SelectListItem
+ {
+ public string Value { get; set; } = "";
+ public string Text { get; set; } = "";
+ }
+
+ private class Account
+ {
+ public int Id { get; set; }
+ public string AccountName { get; set; } = "";
+ }
+
+ private class Measurement
+ {
+ public int Id { get; set; }
+ public string Description { get; set; } = "";
+ }
+
+ private class ItemCategory
+ {
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ }
+
+ private class ItemTaxGroup
+ {
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Inventory/Items.razor b/src/AccountGoWeb/Components/Pages/Inventory/Items.razor
new file mode 100644
index 000000000..005420958
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Inventory/Items.razor
@@ -0,0 +1,261 @@
+@using Dto.Inventory
+@inject HttpClient Http
+
+
+
+
+
+
+
+
+
+ @if (isLoading)
+ {
+
+
+
+ Loading...
+
+
Loading items...
+
+
+ }
+
+ else if (errorMessage != null)
+
+ {
+
+ }
+
+ else if (items == null || !items.Any())
+
+ {
+
+ }
+ else
+ {
+
+
+
+
+
+
+
+ SortBy(nameof(Item.Id))" style="cursor: pointer;">
+ Item @GetSortIcon(nameof(Item.Id))
+
+ SortBy(nameof(Item.Code))" style="cursor: pointer;">
+ Code @GetSortIcon(nameof(Item.Code))
+
+ SortBy(nameof(Item.Description))" style="cursor: pointer;">
+ Description @GetSortIcon(nameof(Item.Description))
+
+ SortBy(nameof(Item.Measurement))" style="cursor: pointer;">
+ Measurement @GetSortIcon(nameof(Item.Measurement))
+
+ SortBy(nameof(Item.ItemTaxGroupName))" style="cursor: pointer;">
+ Item Tax Group @GetSortIcon(nameof(Item.ItemTaxGroupName))
+
+ SortBy(nameof(Item.Cost))" style="cursor: pointer;">
+ Cost @GetSortIcon(nameof(Item.Cost))
+
+ SortBy(nameof(Item.Price))" style="cursor: pointer;">
+ Price @GetSortIcon(nameof(Item.Price))
+
+ SortBy(nameof(Item.QuantityOnHand))" style="cursor: pointer;">
+ On Hand @GetSortIcon(nameof(Item.QuantityOnHand))
+
+
+
+
+
+ @foreach (var item in items)
+ {
+ SelectItem(item)"
+ class="@(selectedItem?.Id == item.Id ? "table-active" : "")" style="cursor: pointer;">
+
+
+
+ @item.Id
+
+
+
+ @item.Code
+ @item.Description
+ @item.Measurement
+ @item.ItemTaxGroupName
+
+ @item.Cost?.ToString("N2")
+ @item.Price?.ToString("N2")
+ @item.QuantityOnHand?.ToString("N2")
+
+ }
+
+
+
+
+
+
+
+
+
+
Total: @items.Count() item(s)
+
+
+ }
+
+
+@code {
+ private List- ? items;
+ private List
- ? allItems;
+ private Item? selectedItem;
+ private bool isLoading = true;
+ private string? errorMessage;
+ private string? sortColumn;
+ private bool sortAscending = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadItems();
+ }
+
+ private async Task LoadItems()
+ {
+ isLoading = true;
+ errorMessage = null;
+
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + "inventory/items";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ allItems = await response.Content.ReadFromJsonAsync
>();
+ items = allItems;
+ }
+ else
+ {
+ errorMessage = $"Failed to load items. Status: {response.StatusCode}";
+ }
+ }
+
+ catch (HttpRequestException ex)
+ {
+ errorMessage = $"Network error: {ex.Message}. Please ensure the API is running on port 8001.";
+ }
+
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading items: {ex.Message}";
+ }
+
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SelectItem(Item item)
+ {
+ selectedItem = item;
+ }
+
+ private void SortBy(string column)
+ {
+ if (items == null) return;
+
+ if (sortColumn == column)
+ {
+ sortAscending = !sortAscending;
+ }
+ else
+ {
+ sortColumn = column;
+ sortAscending = true;
+ }
+
+ items = column switch
+ {
+ nameof(Item.Id) => sortAscending
+ ? items.OrderBy(i => i.Id).ToList()
+ : items.OrderByDescending(i => i.Id).ToList(),
+
+ nameof(Item.Code) => sortAscending
+ ? items.OrderBy(i => i.Code).ToList()
+ : items.OrderByDescending(i => i.Code).ToList(),
+
+ nameof(Item.Description) => sortAscending
+ ? items.OrderBy(i => i.Description).ToList()
+ : items.OrderByDescending(i => i.Description).ToList(),
+
+ nameof(Item.Measurement) => sortAscending
+ ? items.OrderBy(i => i.Measurement).ToList()
+ : items.OrderByDescending(i => i.Measurement).ToList(),
+
+ nameof(Item.ItemTaxGroupName) => sortAscending
+ ? items.OrderBy(i => i.ItemTaxGroupName).ToList()
+ : items.OrderByDescending(i => i.ItemTaxGroupName).ToList(),
+
+ nameof(Item.Cost) => sortAscending
+ ? items.OrderBy(i => i.Cost).ToList()
+ : items.OrderByDescending(i => i.Cost).ToList(),
+
+ nameof(Item.Price) => sortAscending
+ ? items.OrderBy(i => i.Price).ToList()
+ : items.OrderByDescending(i => i.Price).ToList(),
+
+ nameof(Item.QuantityOnHand) => sortAscending
+ ? items.OrderBy(i => i.QuantityOnHand).ToList()
+ : items.OrderByDescending(i => i.QuantityOnHand).ToList(),
+ _ => items
+ };
+ }
+
+ private string GetSortIcon(string column)
+ {
+ if (sortColumn != column) return "";
+ return sortAscending ? "▲" : "▼";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoiceForm.razor b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoiceForm.razor
new file mode 100644
index 000000000..3ecf789e6
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoiceForm.razor
@@ -0,0 +1,441 @@
+@rendermode InteractiveServer
+@using System.Text.Json
+@inject IHttpClientFactory ClientFactory
+@inject IConfiguration Configuration
+@inject NavigationManager NavigationManager
+@inject IJSRuntime JSRuntime
+
+
+
+@if (loadError)
+{
+ Unable to load purchase invoice. Please try again.
+}
+else if (purchaseInvoice is null)
+{
+ Loading...
+}
+else
+{
+
+ @if (InvoiceId > 0)
+ {
+
+ @(isViewMode ? "Edit" : "Cancel")
+
+ }
+
+
+
+
+
+
+ @if (saveError)
+ {
+
+ Error saving purchase invoice.
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+ Details: @errorMessage
+ }
+
+ }
+ @if (saveSuccess)
+ {
+ Purchase invoice saved successfully!
+ }
+}
+
+@code {
+ [Parameter]
+ public int InvoiceId { get; set; } = 0;
+
+ private PurchaseInvoiceDto? purchaseInvoice;
+ private List? vendors, items, measurements;
+ private bool loadError, saveError, saveSuccess, isViewMode = true, isSaving = false;
+ private string? errorMessage;
+
+ public class SelectListItem { public string Value { get; set; } = ""; public string Text { get; set; } = ""; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadReferenceData();
+ if (InvoiceId > 0)
+ {
+ await LoadPurchaseInvoice(InvoiceId);
+ isViewMode = true;
+ }
+ else
+ {
+ InitializeNewInvoice();
+ isViewMode = false;
+ }
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("fixTableBackground");
+ }
+ catch { }
+ }
+ }
+
+ private void InitializeNewInvoice()
+ {
+ var random = new Random();
+ purchaseInvoice = new PurchaseInvoiceDto
+ {
+ No = random.Next(1, 99999).ToString(),
+ InvoiceDate = DateTime.Now,
+ ReferenceNo = "",
+ AmountPaid = 0,
+ IsPaid = false,
+ Posted = false,
+ PurchaseInvoiceLines = new List
+ {
+ new PurchaseInvoiceLineDto { Quantity = 1, Amount = 0, Discount = 0 }
+ }
+ };
+ }
+
+ private async Task LoadReferenceData()
+ {
+ try
+ {
+ string apiurl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+
+ var vendorResponse = await client.GetAsync($"{apiurl}purchasing/vendors");
+ if (vendorResponse.IsSuccessStatusCode)
+ {
+ var json = await vendorResponse.Content.ReadAsStringAsync();
+ var list = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ vendors = list?.Select(v => new SelectListItem { Value = v.Id.ToString(), Text = v.Name ?? "" }).ToList();
+ }
+
+ var itemResponse = await client.GetAsync($"{apiurl}inventory/items");
+ if (itemResponse.IsSuccessStatusCode)
+ {
+ var json = await itemResponse.Content.ReadAsStringAsync();
+ var list = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ items = list?.Select(i => new SelectListItem { Value = i.Id.ToString(), Text = i.Description ?? "" }).ToList();
+ }
+
+ var measurementResponse = await client.GetAsync($"{apiurl}common/measurements");
+ if (measurementResponse.IsSuccessStatusCode)
+ {
+ var json = await measurementResponse.Content.ReadAsStringAsync();
+ var list = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ measurements = list?.Select(m => new SelectListItem { Value = m.Id.ToString(), Text = m.Description ?? "" }).ToList();
+ }
+ }
+ catch (Exception)
+ {
+ vendors = new List();
+ items = new List();
+ measurements = new List();
+ }
+ }
+
+ private async Task LoadPurchaseInvoice(int id)
+ {
+ try
+ {
+ string apiurl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}purchasing/purchaseinvoice?id={id}");
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync();
+ var tempInvoice = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+
+ // Workaround: Try to get ReferenceNo from localStorage if API doesn't return it
+ if (tempInvoice != null)
+ {
+ purchaseInvoice = tempInvoice;
+
+ // If API doesn't return ReferenceNo, try to get it from the list
+ if (string.IsNullOrEmpty(purchaseInvoice.ReferenceNo))
+ {
+ try
+ {
+ var listResponse = await client.GetAsync($"{apiurl}purchasing/purchaseinvoices");
+ if (listResponse.IsSuccessStatusCode)
+ {
+ var listJson = await listResponse.Content.ReadAsStringAsync();
+ var invoicesList = JsonSerializer.Deserialize>(listJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ var invoiceFromList = invoicesList?.FirstOrDefault(i => i.Id == id);
+ if (invoiceFromList != null && !string.IsNullOrEmpty(invoiceFromList.ReferenceNo))
+ {
+ purchaseInvoice.ReferenceNo = invoiceFromList.ReferenceNo;
+ }
+ }
+ }
+ catch { }
+ }
+ }
+ }
+ else
+ {
+ loadError = true;
+ }
+ }
+ catch (Exception)
+ {
+ loadError = true;
+ }
+ }
+
+ private async Task HandleSubmit()
+ {
+ try
+ {
+ isSaving = true;
+ saveError = false;
+ saveSuccess = false;
+ errorMessage = null;
+
+ if (purchaseInvoice != null && purchaseInvoice.PurchaseInvoiceLines != null)
+ {
+ var validLines = purchaseInvoice.PurchaseInvoiceLines
+ .Where(line => line.ItemId.HasValue && line.ItemId.Value > 0)
+ .ToList();
+
+ if (validLines.Count == 0)
+ {
+ errorMessage = "Please add at least one item line with a valid item selected.";
+ saveError = true;
+ isSaving = false;
+ return;
+ }
+
+ purchaseInvoice.PurchaseInvoiceLines = validLines;
+ }
+
+ string apiurl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(purchaseInvoice);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+ var response = await client.PostAsync($"{apiurl}purchasing/savepurchaseinvoice", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ saveSuccess = true;
+ StateHasChanged();
+ await Task.Delay(500);
+ NavigationManager.NavigateTo("/purchasing/purchaseinvoices", forceLoad: true);
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"API returned {response.StatusCode}: {errorContent}";
+ saveError = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+ saveError = true;
+ }
+ finally
+ {
+ isSaving = false;
+ }
+ }
+
+ private void ToggleEdit() { isViewMode = !isViewMode; }
+ private void AddLine() { purchaseInvoice?.PurchaseInvoiceLines?.Add(new PurchaseInvoiceLineDto { Quantity = 1, Amount = 0, Discount = 0 }); }
+ private void RemoveLine(int index)
+ {
+ if (purchaseInvoice?.PurchaseInvoiceLines != null && purchaseInvoice.PurchaseInvoiceLines.Count > 1)
+ purchaseInvoice.PurchaseInvoiceLines.RemoveAt(index);
+ }
+
+ public class PurchaseInvoiceDto
+ {
+ public int Id { get; set; }
+ public string? No { get; set; }
+ public int VendorId { get; set; }
+ public DateTime InvoiceDate { get; set; }
+ public decimal AmountPaid { get; set; }
+ public bool IsPaid { get; set; }
+ public bool Posted { get; set; }
+ public string? VendorInvoiceNo { get; set; }
+ public string? ReferenceNo { get; set; }
+ public List PurchaseInvoiceLines { get; set; } = new();
+ }
+
+ public class PurchaseInvoiceLineDto
+ {
+ public int Id { get; set; }
+ public int? ItemId { get; set; }
+ public int? MeasurementId { get; set; }
+ public decimal? Quantity { get; set; }
+ public decimal? Amount { get; set; }
+ public decimal? Discount { get; set; }
+ }
+
+ public class VendorDto { public int Id { get; set; } public string? Name { get; set; } }
+ public class ItemDto { public int Id { get; set; } public string? Description { get; set; } }
+ public class MeasurementDto { public int Id { get; set; } public string? Description { get; set; } }
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoiceForm.razor.css b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoiceForm.razor.css
new file mode 100644
index 000000000..9dd4ffb54
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoiceForm.razor.css
@@ -0,0 +1,32 @@
+.card-body .row .col-sm-3 {
+ color: white;
+ font-weight: 500;
+ padding-top: 0.375rem;
+}
+
+.table thead th {
+ color: white;
+ background-color: #343a40;
+}
+
+.invoice-form-wrapper .table-container .table {
+ --bs-table-hover-bg: transparent;
+ --bs-table-active-bg: transparent;
+ --bs-table-striped-bg: transparent;
+}
+
+.invoice-form-wrapper .table-container table tbody tr,
+.invoice-form-wrapper .table-container table tbody tr td {
+ background-color: transparent !important;
+}
+
+.invoice-form-wrapper .table-container table tbody tr td select:focus,
+.invoice-form-wrapper .table-container table tbody tr td input:focus,
+.invoice-form-wrapper .table-container table tbody tr td input[type="number"]:focus,
+.invoice-form-wrapper .table-container table tbody tr td .form-control:focus {
+ border: 2px solid #0d6efd !important;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25) !important;
+ background-color: #fff !important;
+ outline: none !important;
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoicesList.razor b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoicesList.razor
new file mode 100644
index 000000000..29bb84498
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoicesList.razor
@@ -0,0 +1,153 @@
+@rendermode InteractiveServer
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@inject IHttpClientFactory ClientFactory
+
+@if (getError)
+{
+
+ Unable to get data. Please try again later.
+
+}
+else if (purchaseInvoices is null)
+{
+ Loading...
+}
+else
+{
+
+
+
+
+
+
+ No
+ Vendor Name
+ Invoice Date
+ Amount
+ Amount Paid
+ Ref No
+ Actions
+
+
+
+ @foreach (var invoice in purchaseInvoices)
+ {
+ SelectInvoice(invoice)"
+ class="@(selectedInvoice?.Id == invoice.Id ? "table-active" : "")"
+ style="cursor: pointer;">
+ @invoice.No
+ @invoice.VendorName
+ @invoice.InvoiceDate.ToString("yyyy-MM-dd")
+ @("$" + invoice.Amount.ToString("N2"))
+ @("$" + invoice.AmountPaid.ToString("N2"))
+ @invoice.ReferenceNo
+
+
+ View
+
+ @if (!invoice.IsPaid && invoice.Posted)
+ {
+
+ Pay
+
+ }
+
+
+ }
+
+
+
+}
+
+@code {
+ private List? purchaseInvoices;
+ private PurchaseInvoiceDto? selectedInvoice;
+ private bool getError;
+ private bool shouldRender = true;
+
+ protected override bool ShouldRender() => shouldRender;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadPurchaseInvoices();
+ }
+
+ private async Task LoadPurchaseInvoices()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var request = new HttpRequestMessage(HttpMethod.Get, $"{apiurl}purchasing/purchaseinvoices");
+ request.Headers.Add("Accept", "application/json");
+
+ var client = ClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Accept.Clear();
+ client.DefaultRequestHeaders.Clear();
+ client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { NoCache = true };
+ client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
+
+ var response = await client.SendAsync(request);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ purchaseInvoices = JsonSerializer.Deserialize>(responseString, options);
+ }
+ else
+ {
+ getError = true;
+ }
+ }
+ catch (Exception)
+ {
+ getError = true;
+ }
+
+ shouldRender = true;
+ }
+
+ private void SelectInvoice(PurchaseInvoiceDto invoice)
+ {
+ selectedInvoice = invoice;
+ StateHasChanged();
+ }
+
+ public class PurchaseInvoiceDto
+ {
+ public int Id { get; set; }
+ public string? No { get; set; }
+ public int VendorId { get; set; }
+ public string? VendorName { get; set; }
+ public DateTime InvoiceDate { get; set; }
+ public decimal Amount { get; set; }
+ public decimal AmountPaid { get; set; }
+ public string? ReferenceNo { get; set; }
+ public bool Posted { get; set; }
+ public bool IsPaid { get; set; }
+ }
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoicesList.razor.css b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoicesList.razor.css
new file mode 100644
index 000000000..9adbc4212
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseInvoicesList.razor.css
@@ -0,0 +1,20 @@
+.table {
+ --bs-table-hover-bg: rgba(255, 255, 255, 0.05);
+ --bs-table-active-bg: rgba(13, 110, 253, 0.2);
+ --bs-table-striped-bg: transparent;
+}
+
+.table tbody tr.table-active,
+.table tbody tr.table-active td {
+ background-color: rgba(13, 110, 253, 0.2) !important;
+ border-left: 3px solid #0d6efd;
+}
+
+.table tbody tr:hover {
+ background-color: rgba(255, 255, 255, 0.05) !important;
+}
+
+.table tbody tr.table-active:hover {
+ background-color: rgba(13, 110, 253, 0.25) !important;
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrderForm.razor b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrderForm.razor
new file mode 100644
index 000000000..2384b1ccf
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrderForm.razor
@@ -0,0 +1,466 @@
+@rendermode InteractiveServer
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@inject IHttpClientFactory ClientFactory
+@inject IConfiguration Configuration
+@inject NavigationManager NavigationManager
+@inject IJSRuntime JSRuntime
+
+
+
+@if (loadError)
+{
+
+ Unable to load purchase order. Please try again later.
+
+}
+else if (purchaseOrder is null)
+{
+ Loading...
+}
+else
+{
+
+ @if (OrderId > 0)
+ {
+
+ @(isViewMode ? "Edit" : "Cancel Edit")
+
+ }
+
+
+
+
+
+
+ @if (saveError)
+ {
+
+ Error saving purchase order. Please try again.
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+ Details: @errorMessage
+ }
+
+ }
+ @if (saveSuccess)
+ {
+ Purchase order saved successfully!
+ }
+}
+
+@code {
+ [Parameter]
+ public int OrderId { get; set; } = 0;
+
+ private PurchaseOrderDto? purchaseOrder;
+ private List? vendors;
+ private List? items;
+ private List? measurements;
+ private bool loadError;
+ private bool saveError;
+ private bool saveSuccess;
+ private bool isViewMode = true;
+ private bool isSaving = false;
+ private string? errorMessage;
+
+ public class SelectListItem
+ {
+ public string Value { get; set; } = "";
+ public string Text { get; set; } = "";
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadReferenceData();
+
+ if (OrderId > 0)
+ {
+ await LoadPurchaseOrder(OrderId);
+ isViewMode = true;
+ }
+ else
+ {
+ InitializeNewPurchaseOrder();
+ isViewMode = false;
+ }
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("fixTableBackgroundOrder");
+ }
+ catch { }
+ }
+ }
+
+ private void InitializeNewPurchaseOrder()
+ {
+ var random = new Random();
+ purchaseOrder = new PurchaseOrderDto
+ {
+ No = random.Next(1, 99999).ToString(),
+ OrderDate = DateTime.Now,
+ ReferenceNo = "",
+ Completed = false,
+ PurchaseOrderLines = new List
+ {
+ new PurchaseOrderLineDto { Quantity = 1, Amount = 0, Discount = 0, ItemId = 0, MeasurementId = 0 }
+ }
+ };
+ }
+
+ private async Task LoadReferenceData()
+ {
+ try
+ {
+ string apiurl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+
+ // Load vendors
+ var vendorResponse = await client.GetAsync($"{apiurl}purchasing/vendors");
+ if (vendorResponse.IsSuccessStatusCode)
+ {
+ var vendorJson = await vendorResponse.Content.ReadAsStringAsync();
+ var vendorList = JsonSerializer.Deserialize>(vendorJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ vendors = vendorList?.Select(v => new SelectListItem { Value = v.Id.ToString(), Text = v.Name ?? "" }).ToList();
+ }
+
+ // Load items
+ var itemResponse = await client.GetAsync($"{apiurl}inventory/items");
+ if (itemResponse.IsSuccessStatusCode)
+ {
+ var itemJson = await itemResponse.Content.ReadAsStringAsync();
+ var itemList = JsonSerializer.Deserialize>(itemJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ items = itemList?.Select(i => new SelectListItem { Value = i.Id.ToString(), Text = i.Description ?? "" }).ToList();
+ }
+
+ // Load measurements
+ var measurementResponse = await client.GetAsync($"{apiurl}common/measurements");
+ if (measurementResponse.IsSuccessStatusCode)
+ {
+ var measurementJson = await measurementResponse.Content.ReadAsStringAsync();
+ var measurementList = JsonSerializer.Deserialize>(measurementJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ measurements = measurementList?.Select(m => new SelectListItem { Value = m.Id.ToString(), Text = m.Description ?? "" }).ToList();
+ }
+ }
+ catch (Exception)
+ {
+ vendors = new List();
+ items = new List();
+ measurements = new List();
+ }
+ }
+
+ private async Task LoadPurchaseOrder(int id)
+ {
+ try
+ {
+ string apiurl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}purchasing/purchaseorder?id={id}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync();
+ purchaseOrder = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ }
+ else
+ {
+ loadError = true;
+ }
+ }
+ catch (Exception)
+ {
+ loadError = true;
+ }
+ }
+
+ private async Task HandleSubmit()
+ {
+ try
+ {
+ isSaving = true;
+ saveError = false;
+ saveSuccess = false;
+ errorMessage = null;
+
+ if (purchaseOrder != null && purchaseOrder.PurchaseOrderLines != null)
+ {
+ var validLines = purchaseOrder.PurchaseOrderLines
+ .Where(line => line.ItemId.HasValue && line.ItemId.Value > 0)
+ .ToList();
+
+ if (validLines.Count == 0)
+ {
+ errorMessage = "Please add at least one item line with a valid item selected.";
+ saveError = true;
+ isSaving = false;
+ return;
+ }
+
+ purchaseOrder.PurchaseOrderLines = validLines;
+ }
+
+ string apiurl = Configuration["ApiUrl"] ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(purchaseOrder);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+ var response = await client.PostAsync($"{apiurl}purchasing/savepurchaseorder", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ saveSuccess = true;
+ StateHasChanged();
+ await Task.Delay(500);
+ NavigationManager.NavigateTo("/purchasing/purchaseorders", forceLoad: true);
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"API returned {response.StatusCode}: {errorContent}";
+ saveError = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+ saveError = true;
+ }
+ finally
+ {
+ isSaving = false;
+ }
+ }
+
+ private void ToggleEdit()
+ {
+ isViewMode = !isViewMode;
+ }
+
+ private void AddLine()
+ {
+ purchaseOrder?.PurchaseOrderLines?.Add(new PurchaseOrderLineDto { Quantity = 1, Amount = 0, Discount = 0, ItemId = 0, MeasurementId = 0 });
+ }
+
+ private void RemoveLine(int index)
+ {
+ if (purchaseOrder?.PurchaseOrderLines != null && purchaseOrder.PurchaseOrderLines.Count > 1)
+ {
+ purchaseOrder.PurchaseOrderLines.RemoveAt(index);
+ }
+ }
+
+ // DTOs
+ public class PurchaseOrderDto
+ {
+ public int Id { get; set; }
+ public string? No { get; set; }
+ public int VendorId { get; set; }
+ public DateTime OrderDate { get; set; }
+ public decimal Amount => PurchaseOrderLines?.Sum(l => ((l.Quantity ?? 0) * (l.Amount ?? 0)) * (1 - ((l.Discount ?? 0) / 100))) ?? 0;
+ public bool Completed { get; set; }
+ public string? ReferenceNo { get; set; }
+ public List PurchaseOrderLines { get; set; } = new();
+ }
+
+ public class PurchaseOrderLineDto
+ {
+ public int Id { get; set; }
+ public int? ItemId { get; set; }
+ public int? MeasurementId { get; set; }
+ public decimal? Quantity { get; set; }
+ public decimal? Amount { get; set; }
+ public decimal? Discount { get; set; }
+ }
+
+ public class VendorDto
+ {
+ public int Id { get; set; }
+ public string? Name { get; set; }
+ }
+
+ public class ItemDto
+ {
+ public int Id { get; set; }
+ public string? Description { get; set; }
+ }
+
+ public class MeasurementDto
+ {
+ public int Id { get; set; }
+ public string? Description { get; set; }
+ }
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrderForm.razor.css b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrderForm.razor.css
new file mode 100644
index 000000000..bf127dfd3
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrderForm.razor.css
@@ -0,0 +1,32 @@
+.card-body .row .col-sm-3 {
+ color: white;
+ font-weight: 500;
+ padding-top: 0.375rem;
+}
+
+.table thead th {
+ color: white;
+ background-color: #343a40;
+}
+
+.order-form-wrapper .table-container .table {
+ --bs-table-hover-bg: transparent;
+ --bs-table-active-bg: transparent;
+ --bs-table-striped-bg: transparent;
+}
+
+.order-form-wrapper .table-container table tbody tr,
+.order-form-wrapper .table-container table tbody tr td {
+ background-color: transparent !important;
+}
+
+.order-form-wrapper .table-container table tbody tr td select:focus,
+.order-form-wrapper .table-container table tbody tr td input:focus,
+.order-form-wrapper .table-container table tbody tr td input[type="number"]:focus,
+.order-form-wrapper .table-container table tbody tr td .form-control:focus {
+ border: 2px solid #0d6efd !important;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25) !important;
+ background-color: #fff !important;
+ outline: none !important;
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrdersList.razor b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrdersList.razor
new file mode 100644
index 000000000..1a4bd128d
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrdersList.razor
@@ -0,0 +1,144 @@
+@rendermode InteractiveServer
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@inject IHttpClientFactory ClientFactory
+@inject NavigationManager Navigation
+
+@if (getError)
+{
+
+ Unable to get data. Please try again later.
+
+}
+else if (purchaseOrders is null)
+{
+ Loading...
+}
+else
+{
+
+
+
+
+
+
+ No
+ Vendor Name
+ Order Date
+ Amount
+ Ref No
+ Actions
+
+
+
+ @foreach (var order in purchaseOrders)
+ {
+ SelectOrder(order)"
+ class="@(selectedPurchaseOrder?.Id == order.Id ? "table-active" : "")"
+ style="cursor: pointer;">
+ @order.No
+ @order.VendorName
+ @order.OrderDate.ToString("yyyy-MM-dd")
+ @("$" + order.Amount.ToString("N2"))
+ @order.ReferenceNo
+
+
+ View
+
+
+
+ }
+
+
+
+}
+
+@code {
+ private List? purchaseOrders;
+ private PurchaseOrderDto? selectedPurchaseOrder;
+ private bool getError;
+ private bool shouldRender = true;
+
+ protected override bool ShouldRender() => shouldRender;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadPurchaseOrders();
+ }
+
+ private async Task LoadPurchaseOrders()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var request = new HttpRequestMessage(HttpMethod.Get, $"{apiurl}purchasing/purchaseorders");
+ request.Headers.Add("Accept", "application/json");
+
+ var client = ClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Accept.Clear();
+ client.DefaultRequestHeaders.Clear();
+ client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { NoCache = true };
+ client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
+
+ var response = await client.SendAsync(request);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ purchaseOrders = JsonSerializer.Deserialize>(responseString, options);
+ }
+ else
+ {
+ getError = true;
+ }
+ }
+ catch (Exception)
+ {
+ getError = true;
+ }
+
+ shouldRender = true;
+ }
+
+ private void SelectOrder(PurchaseOrderDto order)
+ {
+ selectedPurchaseOrder = order;
+ StateHasChanged();
+ }
+
+ public class PurchaseOrderDto
+ {
+ public int Id { get; set; }
+ public string? No { get; set; }
+ public int VendorId { get; set; }
+ public string? VendorName { get; set; }
+ public DateTime OrderDate { get; set; }
+ public decimal Amount { get; set; }
+ public string? ReferenceNo { get; set; }
+ public int StatusId { get; set; }
+ }
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrdersList.razor.css b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrdersList.razor.css
new file mode 100644
index 000000000..9adbc4212
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Payables/PurchaseOrdersList.razor.css
@@ -0,0 +1,20 @@
+.table {
+ --bs-table-hover-bg: rgba(255, 255, 255, 0.05);
+ --bs-table-active-bg: rgba(13, 110, 253, 0.2);
+ --bs-table-striped-bg: transparent;
+}
+
+.table tbody tr.table-active,
+.table tbody tr.table-active td {
+ background-color: rgba(13, 110, 253, 0.2) !important;
+ border-left: 3px solid #0d6efd;
+}
+
+.table tbody tr:hover {
+ background-color: rgba(255, 255, 255, 0.05) !important;
+}
+
+.table tbody tr.table-active:hover {
+ background-color: rgba(13, 110, 253, 0.25) !important;
+}
+
diff --git a/src/AccountGoWeb/Components/Pages/Purchasing/VendorForm.razor b/src/AccountGoWeb/Components/Pages/Purchasing/VendorForm.razor
new file mode 100644
index 000000000..7471abb99
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Purchasing/VendorForm.razor
@@ -0,0 +1,406 @@
+@using Dto.Purchasing
+@using Dto.Common
+@using Microsoft.AspNetCore.Components.Forms
+@inject IHttpClientFactory HttpClientFactory
+@inject NavigationManager Navigation
+
+
+
+
+
+
+
+ @* General Section *@
+
+
+ @* Contact Section *@
+
+
+ @* Invoicing Section *@
+
+
+
+
+
+
Accounts Payable
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
Purchase
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
Discount
+
+
+ Select...
+ @foreach (var account in accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+
+
Tax Group
+
+
+ Select...
+ @foreach (var taxGroup in taxGroups)
+ {
+ @taxGroup.Text
+ }
+
+
+
+
+
+
+
+ @* Payment Section *@
+
+
+
+
+
+
Payment Term
+
+
+ Select...
+ @foreach (var term in paymentTerms)
+ {
+ @term.Text
+ }
+
+
+
+
+
+
+
+
+
+
+ @if (isSaving)
+ {
+
+ }
+ Save
+
+
Close
+
+ @if (!string.IsNullOrEmpty(errorMessage))
+ {
+
@errorMessage
+ }
+ @if (!string.IsNullOrEmpty(successMessage))
+ {
+
@successMessage
+ }
+
+
+
+@code {
+ [Parameter]
+ public int VendorId { get; set; }
+
+ private Vendor vendor = new Vendor();
+ private bool isEditMode = false;
+ private bool isSaving = false;
+ private string? errorMessage;
+ private string? successMessage;
+
+ private List accounts = new();
+ private List taxGroups = new();
+ private List paymentTerms = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadDropdownData();
+
+ if (VendorId == -1 || VendorId == 0)
+ {
+ // New vendor
+ isEditMode = true;
+ vendor.No = new Random().Next(1, 99999).ToString();
+ vendor.PrimaryContact = new Contact();
+ }
+ else
+ {
+ // Edit existing vendor
+ await LoadVendor();
+ }
+ }
+
+ private async Task LoadVendor()
+ {
+ try
+ {
+ var http = HttpClientFactory.CreateClient();
+ var apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var response = await http.GetAsync($"{apiUrl}purchasing/vendor?id={VendorId}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ vendor = await response.Content.ReadFromJsonAsync() ?? new Vendor();
+ if (vendor.PrimaryContact == null)
+ {
+ vendor.PrimaryContact = new Contact();
+ }
+ }
+ else
+ {
+ errorMessage = "Failed to load vendor data.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading vendor: {ex.Message}";
+ }
+ }
+
+ private async Task LoadDropdownData()
+ {
+ try
+ {
+ var http = HttpClientFactory.CreateClient();
+ var apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ // Load accounts
+ var accountsResponse = await http.GetAsync($"{apiUrl}financials/accounts");
+ if (accountsResponse.IsSuccessStatusCode)
+ {
+ var accountsList = await accountsResponse.Content.ReadFromJsonAsync>() ?? new();
+ accounts = accountsList.Select(a => new SelectListItem { Value = a.Id.ToString(), Text = a.AccountName }).ToList();
+ }
+
+ // Load tax groups
+ var taxGroupsResponse = await http.GetAsync($"{apiUrl}tax/taxgroups");
+ if (taxGroupsResponse.IsSuccessStatusCode)
+ {
+ var taxGroupsList = await taxGroupsResponse.Content.ReadFromJsonAsync>() ?? new();
+ taxGroups = taxGroupsList.Select(t => new SelectListItem { Value = t.Id.ToString(), Text = t.Description }).ToList();
+ }
+
+ // Load payment terms
+ var paymentTermsResponse = await http.GetAsync($"{apiUrl}financial/paymentterms");
+ if (paymentTermsResponse.IsSuccessStatusCode)
+ {
+ var paymentTermsList = await paymentTermsResponse.Content.ReadFromJsonAsync>() ?? new();
+ paymentTerms = paymentTermsList.Select(p => new SelectListItem
+ {
+ Value = p.Id.ToString(),
+ Text = p.Description
+ }).ToList();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading dropdown data: {ex.Message}";
+ }
+ }
+
+ private void ToggleEdit()
+ {
+ isEditMode = !isEditMode;
+ }
+
+ private async Task HandleValidSubmit()
+ {
+ isSaving = true;
+ errorMessage = null;
+ successMessage = null;
+
+ try
+ {
+ var http = HttpClientFactory.CreateClient();
+ var apiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var response = await http.PostAsJsonAsync($"{apiUrl}purchasing/savevendor", vendor);
+
+ if (response.IsSuccessStatusCode)
+ {
+ successMessage = "Vendor saved successfully!";
+ await Task.Delay(1000);
+ Navigation.NavigateTo("/Purchasing/Vendors");
+ }
+ else
+ {
+ errorMessage = $"Failed to save vendor. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving vendor: {ex.Message}";
+ }
+ finally
+ {
+ isSaving = false;
+ }
+ }
+
+ private class SelectListItem
+ {
+ public string Value { get; set; } = "";
+ public string Text { get; set; } = "";
+ }
+
+ private class Account
+ {
+ public int Id { get; set; }
+ public string AccountName { get; set; } = "";
+ }
+
+ private class TaxGroup
+ {
+ public int Id { get; set; }
+ public string Description { get; set; } = "";
+ }
+
+ private class PaymentTerm
+ {
+ public int Id { get; set; }
+ public string Description { get; set; } = "";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Purchasing/Vendors.razor b/src/AccountGoWeb/Components/Pages/Purchasing/Vendors.razor
new file mode 100644
index 000000000..83e7865d1
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Purchasing/Vendors.razor
@@ -0,0 +1,245 @@
+@using Dto.Purchasing
+@inject HttpClient Http
+
+
+
+
+
+
+
+
+
+ @if (isLoading)
+ {
+
+
+
+ Loading...
+
+
Loading vendors...
+
+
+ }
+
+ else if (errorMessage != null)
+
+ {
+
+ }
+
+ else if (vendors == null || !vendors.Any())
+
+ {
+
+
+
+ No vendors found.
+
+
+
+ }
+ else
+ {
+
+
+
+
+
+
+
+ SortBy(nameof(Vendor.No))" style="cursor: pointer;">
+ No @GetSortIcon(nameof(Vendor.No))
+
+ SortBy(nameof(Vendor.Name))" style="cursor: pointer;">
+ Name @GetSortIcon(nameof(Vendor.Name))
+
+ SortBy(nameof(Vendor.Phone))" style="cursor: pointer;">
+ Phone @GetSortIcon(nameof(Vendor.Phone))
+
+ SortBy(nameof(Vendor.Contact))" style="cursor: pointer;">
+ Contact @GetSortIcon(nameof(Vendor.Contact))
+
+ SortBy(nameof(Vendor.TaxGroup))" style="cursor: pointer;">
+ Tax Group @GetSortIcon(nameof(Vendor.TaxGroup))
+
+ SortBy(nameof(Vendor.Balance))" style="cursor: pointer;">
+ Balance @GetSortIcon(nameof(Vendor.Balance))
+
+
+
+
+
+ @foreach (var vendor in vendors)
+ {
+ SelectVendor(vendor)"
+ class="@(selectedVendor?.Id == vendor.Id ? "table-active" : "")" style="cursor: pointer;">
+
+
+
+ @vendor.No
+
+
+
+ @vendor.Name
+ @vendor.Phone
+ @vendor.Contact
+ @vendor.TaxGroup
+
+ @vendor.Balance.ToString("N2")
+
+ }
+
+
+
+
+
+
+
+
+
+
Total: @vendors.Count() vendor(s)
+
+
+ }
+
+
+@code {
+ private List? vendors;
+ private List? allVendors;
+ private Vendor? selectedVendor;
+ private bool isLoading = true;
+ private string? errorMessage;
+ private string? sortColumn;
+ private bool sortAscending = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadVendors();
+ }
+
+ private async Task LoadVendors()
+ {
+ isLoading = true;
+ errorMessage = null;
+
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + "purchasing/vendors";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ allVendors = await response.Content.ReadFromJsonAsync>();
+ vendors = allVendors;
+ }
+ else
+ {
+ errorMessage = $"Failed to load vendors. Status: {response.StatusCode}";
+ }
+ }
+
+ catch (HttpRequestException ex)
+ {
+ errorMessage = $"Network error: {ex.Message}. Please ensure the API is running on port 8001.";
+ }
+
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading vendors: {ex.Message}";
+ }
+
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SelectVendor(Vendor vendor)
+ {
+ selectedVendor = vendor;
+ }
+
+ private void SortBy(string column)
+ {
+ if (vendors == null) return;
+
+ if (sortColumn == column)
+ {
+ sortAscending = !sortAscending;
+ }
+ else
+ {
+ sortColumn = column;
+ sortAscending = true;
+ }
+
+ vendors = column switch
+ {
+ nameof(Vendor.No) => sortAscending
+ ? vendors.OrderBy(v => v.No).ToList()
+ : vendors.OrderByDescending(v => v.No).ToList(),
+
+ nameof(Vendor.Name) => sortAscending
+ ? vendors.OrderBy(v => v.Name).ToList()
+ : vendors.OrderByDescending(v => v.Name).ToList(),
+
+ nameof(Vendor.Phone) => sortAscending
+ ? vendors.OrderBy(v => v.Phone).ToList()
+ : vendors.OrderByDescending(v => v.Phone).ToList(),
+
+ nameof(Vendor.Contact) => sortAscending
+ ? vendors.OrderBy(v => v.Contact).ToList()
+ : vendors.OrderByDescending(v => v.Contact).ToList(),
+
+ nameof(Vendor.TaxGroup) => sortAscending
+ ? vendors.OrderBy(v => v.TaxGroup).ToList()
+ : vendors.OrderByDescending(v => v.TaxGroup).ToList(),
+
+ nameof(Vendor.Balance) => sortAscending
+ ? vendors.OrderBy(v => v.Balance).ToList()
+ : vendors.OrderByDescending(v => v.Balance).ToList(),
+ _ => vendors
+ };
+ }
+
+ private string GetSortIcon(string column)
+ {
+ if (sortColumn != column) return "";
+ return sortAscending ? "▲" : "▼";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Quotations/AddSalesQuotation.razor b/src/AccountGoWeb/Components/Pages/Quotations/AddSalesQuotation.razor
new file mode 100644
index 000000000..4e9809975
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Quotations/AddSalesQuotation.razor
@@ -0,0 +1,255 @@
+@using Dto.Sales
+@using Microsoft.AspNetCore.Mvc.Rendering
+@using Microsoft.EntityFrameworkCore.Metadata.Internal
+@inject HttpClient Http
+@inject IConfiguration Configuration
+@inject NavigationManager Navigation
+@inject IJSRuntime JSRuntime
+
+Add Sales Quotation
+
+
+
+
+
+
+
+
+
+
+
Customer
+
+
+ Select Customer
+ @foreach (var c in Customers)
+ {
+ @c.Name
+ }
+
+
+
+
+
+
+
Payment Term
+
+
+
+ Payment due within 10 days
+ Due 15th Of the Following Month
+ Cash Only
+
+
+
+
+
+
+
+
+
+
+ Item
+ Quantity
+ Amount
+ Discount
+ Measurement
+
+
+
+ @if (model?.SalesQuotationLines != null)
+ {
+ @for (int i = 0; i < model.SalesQuotationLines.Count; i++)
+ {
+ var line = model.SalesQuotationLines[i];
+
+
+
+
+ HOA Dues
+ Car Sticker
+ Optical Mouse
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Each
+ Hour
+ Monthly
+ Pack
+
+
+
+ }
+ }
+ else
+ {
+
+ Loading...
+
+ }
+
+
+ Add Row
+
+
+
+
+
+
+
+ Save
+ Close
+
+
+
+
+@code {
+private SalesQuotation model = new SalesQuotation
+{
+ QuotationDate = DateTime.Now,
+ SalesQuotationLines = new List
+ {
+ new SalesQuotationLine
+ {
+ ItemId = 1,
+ MeasurementId = 1,
+ Quantity = 1.00m,
+ Amount = 0.00m,
+ Discount = 0.00m
+ }
+ }
+};
+
+ private List Items = new();
+ private List PaymentTerms = new();
+ private List Measurements = new();
+ private List Customers = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ var api = Configuration["ApiUrl"];
+ if (!string.IsNullOrEmpty(api) && !api.EndsWith("/"))
+ api += "/";
+
+ Customers = await Load>(api + "sales/customers");
+ PaymentTerms = await Load>(api + "common/paymentterms");
+ Items = await Load>(api + "inventory/items");
+ Measurements = await Load>(api + "common/measurements");
+ }
+
+
+ private async Task Load(string url)
+ {
+ var response = await Http.GetAsync(url);
+
+ if (!response.IsSuccessStatusCode)
+ return default!;
+
+ var json = await response.Content.ReadAsStringAsync();
+ return System.Text.Json.JsonSerializer.Deserialize(json,
+ new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
+ }
+
+
+
+ private void AddRow()
+ {
+ if (model?.SalesQuotationLines != null)
+ model.SalesQuotationLines.Add(new SalesQuotationLine
+ {
+ ItemId = 1,
+ MeasurementId = 1,
+ Quantity = 1.00m,
+ Amount = 0.00m,
+ Discount = 0.00m
+ });
+ }
+
+
+ private void Close()
+ {
+ Navigation.NavigateTo("/quotations/salesquotations");
+ }
+
+private async Task SaveQuotation()
+{
+ try
+ {
+ // Sanitize model before sending
+ if (model.CustomerId == null || model.CustomerId == 0)
+ {
+ Console.WriteLine("Error: Customer must be selected.");
+ return; // stop saving if customer not selected
+ }
+
+ // Populate dependent fields
+ model.CustomerName = Customers.FirstOrDefault(c => c.Id == model.CustomerId)?.Name;
+ model.QuotationDate = DateTime.Now;
+ model.StatusId = 0; // Draft
+ model.SalesQuoteStatus = "Draft";
+
+ // Ensure all SalesQuotationLines have valid IDs
+ foreach (var line in model.SalesQuotationLines)
+ {
+ if (line.ItemId == 0) line.ItemId = 1; // default to first item
+ if (line.MeasurementId == 0) line.MeasurementId = 1; // default to first measurement
+ if (line.Quantity == 0) line.Quantity = 1.0m;
+ if (line.Amount == 0) line.Amount = 0.0m;
+ if (line.Discount == 0) line.Discount = 0.0m;
+ }
+
+ // Serialize and log JSON
+ var json = System.Text.Json.JsonSerializer.Serialize(model,
+ new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
+ Console.WriteLine("Sending JSON to API:");
+ Console.WriteLine(json);
+
+ // Send to API
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+ var apiBase = Configuration["ApiUrl"] ?? "https://gdbapi.azurewebsites.net/api/";
+ if (!apiBase.EndsWith("/")) apiBase += "/";
+ var endpoint = apiBase + "sales/SaveQuotation";
+
+ var response = await Http.PostAsync(endpoint, content);
+
+ Console.WriteLine($"API response status: {response.StatusCode}");
+ var respContent = await response.Content.ReadAsStringAsync();
+ Console.WriteLine($"API response content: {respContent}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ Console.WriteLine("Quotation saved successfully!");
+ Navigation.NavigateTo("/quotations/salesquotations");
+ }
+ else
+ {
+ Console.WriteLine("Save failed.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Error saving quotation: " + ex.Message);
+ }
+}
+
+
+
+
+ public class ItemDto { public int Id { get; set; } public string Name { get; set; } = string.Empty; }
+ public class PaymentTermDto { public int Id { get; set; } public string Name { get; set; } = string.Empty; }
+ public class MeasurementDto { public int Id { get; set; } public string Name { get; set; } = string.Empty; }
+ public class CustomerDto { public int Id { get; set; } public string Name { get; set; } = string.Empty; }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Quotations/SalesQuotations.razor b/src/AccountGoWeb/Components/Pages/Quotations/SalesQuotations.razor
new file mode 100644
index 000000000..d2630799b
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Quotations/SalesQuotations.razor
@@ -0,0 +1,162 @@
+@inject IHttpClientFactory HttpClientFactory
+@inject IConfiguration Configuration
+@inject NavigationManager NavigationManager
+@inject IJSRuntime JSRuntime
+@inject IJSRuntime JS
+
+@inject HttpClient Http
+
+
+Sales Quotations
+
+
+
+
+Quotations
+
+
+@if (isLoading)
+{
+
+}
+else if (hasError)
+{
+
+
+ Error! Unable to load quotations. Please try again.
+
+}
+else
+{
+
+
+
+
+ No
+ Customer
+ Date
+ Amount
+ Status
+
+
+
+ @foreach (var q in quotations)
+ {
+ OnSelectionChanged(q.id, q.statusId)" class="quotationRow">
+ @q.no
+ @q.customerName
+ @q.quotationDate.ToShortDateString()
+ @q.amount
+ @q.salesQuoteStatus
+
+ }
+
+
+
+}
+@code {
+ private List quotations = new();
+
+ private string viewQuotationLink = "#";
+ private string newOrderLink = "#";
+ private bool isViewLinkActive = false;
+ private bool isNewOrderLinkActive = false;
+ private bool isLoading = true;
+ private bool hasError = false;
+protected override async Task OnInitializedAsync()
+{
+ try
+ {
+ var apiBase = Configuration["ApiUrl"] ?? "https://gdbapi.azurewebsites.net/api/";
+ if (!apiBase.EndsWith("/")) apiBase += "/";
+
+ var endpoint = apiBase + "Sales/Quotations";
+
+ var response = await Http.GetAsync(endpoint);
+
+ if (response.IsSuccessStatusCode)
+ {
+ quotations = await response.Content.ReadFromJsonAsync>() ?? new List();
+ isLoading = false;
+ }
+ else
+ {
+ hasError = true;
+ isLoading = false;
+ Console.WriteLine($"Error fetching quotations: {response.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ hasError = true;
+ isLoading = false;
+ Console.WriteLine($"Exception fetching quotations: {ex.Message}");
+ }
+}
+
+
+ private void OnSelectionChanged(int id, int status)
+ {
+ viewQuotationLink = $"/quotations/quotation?id={id}";
+ isViewLinkActive = true;
+
+ if (status == 3)
+ {
+ isNewOrderLinkActive = false;
+ newOrderLink = "#";
+ }
+ else if (status == 1)
+ {
+ newOrderLink = $"/sales/salesorder?quotationId={id}";
+ isNewOrderLinkActive = true;
+ }
+ }
+
+ // DTO class for deserialization
+ public class QuotationDto
+ {
+ public int id { get; set; }
+ public string no { get; set; } = "";
+ public int statusId { get; set; }
+ public string customerName { get; set; } = "";
+ public DateTime quotationDate { get; set; }
+ public decimal amount { get; set; }
+ public string salesQuoteStatus { get; set; } = "";
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Sales/AddReceipt.razor b/src/AccountGoWeb/Components/Pages/Sales/AddReceipt.razor
new file mode 100644
index 000000000..a222a637b
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/AddReceipt.razor
@@ -0,0 +1,372 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@using AccountGoWeb.Models
+@using AccountGoWeb.Models.Sales
+@using Microsoft.AspNetCore.Components
+
+
+
+
+
+
+ @if (loading)
+ {
+
Loading...
+ }
+ else if (!string.IsNullOrEmpty(errorMessage))
+ {
+
+ Error: @errorMessage
+
+ }
+ else
+ {
+
+ }
+
+
+@code {
+ private bool loading = true;
+ private string errorMessage = "";
+ private bool submitAttempted = false;
+ private int? customerId = null;
+ private DateTime receiptDate = DateTime.Now;
+ private int? accountToDebitId = null;
+ private int? accountToCreditId = null;
+ private decimal amount = 0;
+
+ private List<(string Text, string Value)> customers = new();
+ private List<(string Text, string Value)> debitAccounts = new();
+ private List<(string Text, string Value)> creditAccounts = new();
+ private Dictionary customerAdvanceAccounts = new();
+ private Dictionary accountNames = new();
+
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60);
+
+ // Load customers from API
+ var customersResponse = await client.GetAsync($"{apiurl}sales/customers");
+ if (customersResponse.IsSuccessStatusCode)
+ {
+ var json = await customersResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var customersData = JsonSerializer.Deserialize>(json, options);
+ if (customersData != null)
+ {
+ foreach (var customer in customersData)
+ {
+ var id = GetJsonPropertyString(customer, "id");
+ var name = GetJsonPropertyString(customer, "name");
+ var prepaymentAccountId = GetJsonPropertyString(customer, "prepaymentAccountId");
+ customers.Add((name ?? "Unknown", id));
+
+ // Store the prepayment account ID for this customer
+ if (int.TryParse(id, out var customerId_int) && int.TryParse(prepaymentAccountId, out var accountId_int))
+ {
+ customerAdvanceAccounts[customerId_int] = accountId_int;
+ }
+ }
+ }
+ }
+
+ // Load debit accounts from API (Cash & Banks)
+ var debitResponse = await client.GetAsync($"{apiurl}financials/CashBanks");
+ if (debitResponse.IsSuccessStatusCode)
+ {
+ var json = await debitResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var accountsData = JsonSerializer.Deserialize>(json, options);
+ if (accountsData != null)
+ {
+ foreach (var account in accountsData)
+ {
+ var id = GetJsonPropertyString(account, "id");
+ var name = GetJsonPropertyString(account, "name");
+ debitAccounts.Add((name ?? "Unknown", id));
+ }
+ }
+ }
+
+ // Load credit accounts from API (posting accounts only)
+ var creditResponse = await client.GetAsync($"{apiurl}common/postingaccounts");
+ if (creditResponse.IsSuccessStatusCode)
+ {
+ var json = await creditResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var accountsData = JsonSerializer.Deserialize>(json, options);
+ if (accountsData != null)
+ {
+ foreach (var account in accountsData)
+ {
+ var id = GetJsonPropertyString(account, "id");
+ var name = GetJsonPropertyString(account, "accountName");
+ if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(name))
+ {
+ creditAccounts.Add((name, id));
+ // Store account name for display
+ if (int.TryParse(id, out var accountId))
+ {
+ accountNames[accountId] = name;
+ }
+ }
+ }
+ }
+ }
+
+ // Also load all accounts to get customer advance account names
+ var allAccountsResponse = await client.GetAsync($"{apiurl}financials/accounts");
+ if (allAccountsResponse.IsSuccessStatusCode)
+ {
+ var json = await allAccountsResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var accountsData = JsonSerializer.Deserialize>(json, options);
+ if (accountsData != null)
+ {
+ foreach (var account in accountsData)
+ {
+ var id = GetJsonPropertyString(account, "id");
+ var name = GetJsonPropertyString(account, "name");
+ if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(name))
+ {
+ // Store account name for display
+ if (int.TryParse(id, out var accountId))
+ {
+ accountNames[accountId] = name;
+ }
+ }
+ }
+ }
+ }
+
+ loading = false;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Failed to load form data: {ex.Message}";
+ loading = false;
+ }
+ }
+
+ private async Task OnCustomerChanged(ChangeEventArgs e)
+ {
+ if (e.Value == null || string.IsNullOrEmpty(e.Value.ToString()))
+ {
+ customerId = null;
+ accountToCreditId = null;
+ return;
+ }
+
+ if (int.TryParse(e.Value.ToString(), out var selectedCustomerId))
+ {
+ customerId = selectedCustomerId;
+
+ // Auto-select the customer's prepayment account
+ if (customerAdvanceAccounts.TryGetValue(selectedCustomerId, out var advanceAccountId))
+ {
+ accountToCreditId = advanceAccountId;
+ }
+ }
+ }
+ private async Task HandleSubmit()
+ {
+ submitAttempted = true;
+
+ // Validate form
+ if (string.IsNullOrEmpty(customerId?.ToString()) ||
+ string.IsNullOrEmpty(accountToDebitId?.ToString()) ||
+ !IsValidCreditAccount() ||
+ amount <= 0)
+ {
+ return;
+ }
+
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+
+ // Create an object with the form data
+ // Ensure all IDs are integers
+ var receiptData = new
+ {
+ CustomerId = customerId.HasValue ? customerId.Value : 0,
+ ReceiptDate = receiptDate,
+ AccountToDebitId = accountToDebitId.HasValue ? accountToDebitId.Value : 0,
+ AccountToCreditId = accountToCreditId.HasValue ? accountToCreditId.Value : 0,
+ Amount = amount
+ };
+
+ // Submit to API
+ var json = System.Text.Json.JsonSerializer.Serialize(receiptData);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync($"{apiurl}sales/savereceipt", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Navigation.NavigateTo("/Sales/SalesReceipts", forceLoad: true);
+ }
+ else
+ {
+ var responseContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save receipt. Status: {response.StatusCode}. Details: {responseContent}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving receipt: {ex.Message}";
+ }
+ }
+
+ private bool IsValidCreditAccount()
+ {
+ // Credit account must be selected
+ if (!accountToCreditId.HasValue || accountToCreditId <= 0)
+ {
+ return false;
+ }
+
+ // Credit account must match the customer's advance account
+ if (customerId.HasValue && customerAdvanceAccounts.TryGetValue(customerId.Value, out var advanceAccountId))
+ {
+ return accountToCreditId == advanceAccountId;
+ }
+
+ // If no customer selected, we can't validate
+ return false;
+ }
+
+ private string GetJsonPropertyString(JsonElement element, string propertyName)
+ {
+ if (element.TryGetProperty(propertyName, out var property))
+ {
+ if (property.ValueKind == JsonValueKind.String)
+ {
+ return property.GetString() ?? "";
+ }
+ else if (property.ValueKind == JsonValueKind.Number)
+ {
+ return property.GetInt32().ToString();
+ }
+ }
+ return "";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/Allocate.razor b/src/AccountGoWeb/Components/Pages/Sales/Allocate.razor
new file mode 100644
index 000000000..a8b32e12f
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/Allocate.razor
@@ -0,0 +1,419 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+
+
+
+
+
+
+ @if (loading)
+ {
+
Loading allocation data...
+ }
+ else if (errorMessage != null)
+ {
+
+ Error: @errorMessage
+
+ }
+ else
+ {
+
+ }
+
+
+@code {
+ [Parameter]
+ public string? ReceiptId { get; set; }
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ private bool loading = true;
+ private string? errorMessage = null;
+ private string? successMessage = null;
+
+ private string receiptNo = "";
+ private string customerName = "";
+ private DateTime receiptDate = DateTime.Now;
+ private decimal amount = 0;
+ private decimal remainingAmount = 0;
+ private int customerId = 0;
+
+ private List allocationLines = new();
+
+ private class AllocationLineModel
+ {
+ public int? InvoiceId { get; set; }
+ public decimal? Amount { get; set; }
+ public decimal? AllocatedAmount { get; set; }
+ public decimal? AmountToAllocate { get; set; }
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Extract ReceiptId from the current URL path
+ // URL format: /Sales/Allocate/123
+ var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
+ var segments = uri.AbsolutePath.Split('/');
+
+ // Find the ID from the URL segments (last segment should be the ID)
+ if (segments.Length > 0 && int.TryParse(segments[^1], out var receiptId))
+ {
+ ReceiptId = receiptId.ToString();
+ }
+
+ // Fallback to query parameter if not found in route
+ if (string.IsNullOrEmpty(ReceiptId))
+ {
+ var queryParams = System.Web.HttpUtility.ParseQueryString(uri.Query);
+ ReceiptId = queryParams["id"];
+ }
+
+ if (!string.IsNullOrEmpty(ReceiptId))
+ {
+ await LoadAllocationData();
+ }
+ else
+ {
+ errorMessage = "Receipt ID is required.";
+ loading = false;
+ }
+ }
+
+ private async Task LoadAllocationData()
+ {
+ try
+ {
+ loading = true;
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60);
+
+ // Fetch receipt details
+ var receiptResponse = await client.GetAsync($"{apiurl}sales/salesreceipt?id={ReceiptId}");
+ if (receiptResponse.IsSuccessStatusCode)
+ {
+ var receiptString = await receiptResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var receiptJson = JsonSerializer.Deserialize(receiptString, options);
+
+ receiptNo = GetJsonProperty(receiptJson, "receiptNo");
+ customerName = GetJsonProperty(receiptJson, "customerName");
+ amount = GetJsonPropertyDecimal(receiptJson, "amount");
+ remainingAmount = GetJsonPropertyDecimal(receiptJson, "remainingAmountToAllocate");
+
+ if (receiptJson.TryGetProperty("customerId", out var custIdProp))
+ {
+ if (custIdProp.TryGetInt32(out var id))
+ customerId = id;
+ }
+
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Loaded Receipt - ReceiptNo: {receiptNo}, CustomerId: {customerId}");
+
+ if (receiptJson.TryGetProperty("receiptDate", out var dateProp))
+ {
+ if (DateTime.TryParse(dateProp.GetString(), out var date))
+ receiptDate = date;
+ }
+
+ // Fetch customer invoices
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Fetching invoices for customerId: {customerId}");
+ await LoadInvoicesForCustomer(customerId, client, apiurl, options);
+ }
+ else
+ {
+ errorMessage = "Failed to load receipt details.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading allocation data: {ex.Message}";
+ }
+ finally
+ {
+ loading = false;
+ }
+ }
+
+ private async Task LoadInvoicesForCustomer(int custId, HttpClient? client = null, string? apiurl = null,
+ JsonSerializerOptions? options = null)
+ {
+ if (client == null)
+ {
+ client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60);
+ }
+
+ if (string.IsNullOrEmpty(apiurl))
+ {
+ apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ }
+
+ if (options == null)
+ {
+ options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ }
+
+ try
+ {
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Fetching invoices for customerId: {custId}");
+ var invoiceResponse = await client.GetAsync($"{apiurl}sales/customerinvoices?id={custId}");
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Invoice API Response Status: {invoiceResponse.StatusCode}");
+
+ if (invoiceResponse.IsSuccessStatusCode)
+ {
+ var invoicesString = await invoiceResponse.Content.ReadAsStringAsync();
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Invoice Response: {invoicesString}");
+ var invoicesJson = JsonSerializer.Deserialize>(invoicesString, options);
+
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Parsed {invoicesJson?.Count ?? 0} invoices");
+ allocationLines.Clear();
+ if (invoicesJson != null)
+ {
+ foreach (var invoice in invoicesJson)
+ {
+ var posted = false;
+ var totalAllocated = 0m;
+ var invoiceAmount = 0m;
+
+ if (invoice.TryGetProperty("posted", out var postedProp))
+ posted = postedProp.GetBoolean();
+
+ if (invoice.TryGetProperty("totalAllocatedAmount", out var allocProp))
+ totalAllocated = allocProp.GetDecimal();
+
+ if (invoice.TryGetProperty("amount", out var amountProp))
+ invoiceAmount = amountProp.GetDecimal();
+
+ // Include invoices with remaining balance
+ // TODO: Should filter for posted invoices only (posted == true), but currently allowing unposted for testing
+ // Invoices must be posted to the GL before they can be allocated.
+ // A "Post Invoice" feature should be added to SalesInvoice component.
+ if (totalAllocated < invoiceAmount)
+ {
+ var line = new AllocationLineModel
+ {
+ InvoiceId = GetJsonPropertyInt(invoice, "id"),
+ Amount = invoiceAmount,
+ AllocatedAmount = totalAllocated,
+ AmountToAllocate = null
+ };
+ allocationLines.Add(line);
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Added invoice {line.InvoiceId} - Posted: {posted}, Allocated: {totalAllocated}, Amount: {invoiceAmount}");
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine($"DEBUG: Skipped invoice - Posted: {posted}, Allocated: {totalAllocated}, Amount: {invoiceAmount}");
+ }
+ }
+ }
+ }
+ else
+ {
+ var errorContent = await invoiceResponse.Content.ReadAsStringAsync();
+ errorMessage = "Failed to load customer invoices.";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading customer invoices: {ex.Message}";
+ }
+ }
+
+ private async Task HandleSubmit()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var payload = new
+ {
+ CustomerId = customerId,
+ ReceiptId = int.Parse(ReceiptId ?? "0"),
+ Date = receiptDate,
+ Amount = amount,
+ RemainingAmountToAllocate = remainingAmount,
+ AllocationLines = allocationLines
+ .Where(l => l.AmountToAllocate.HasValue && l.AmountToAllocate.Value > 0)
+ .Select(l => new
+ {
+ InvoiceId = l.InvoiceId,
+ Amount = l.Amount,
+ AllocatedAmount = l.AllocatedAmount,
+ AmountToAllocate = l.AmountToAllocate
+ })
+ .ToList()
+ };
+
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(payload);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync($"{apiurl}sales/saveallocation", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ // Redirect to SalesReceipts page with forceLoad to refresh the MVC view
+ Navigation.NavigateTo("/Sales/SalesReceipts", forceLoad: true);
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save allocation. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving allocation: {ex.Message}";
+ }
+ }
+
+ private string GetJsonProperty(System.Text.Json.JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ return prop.GetString() ?? "";
+ return "";
+ }
+
+ private decimal GetJsonPropertyDecimal(System.Text.Json.JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.TryGetDecimal(out var val))
+ return val;
+ }
+ return 0;
+ }
+
+ private int GetJsonPropertyInt(System.Text.Json.JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.TryGetInt32(out var val))
+ return val;
+ }
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/Customer.razor b/src/AccountGoWeb/Components/Pages/Sales/Customer.razor
new file mode 100644
index 000000000..bfbc6b549
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/Customer.razor
@@ -0,0 +1,483 @@
+@using CustomerDto = Dto.Sales.Customer
+@using Dto.Common
+@inject HttpClient Http
+@inject NavigationManager Navigation
+@rendermode InteractiveServer
+
+@(Id == 0 ? "New" : "Edit") Customer
+
+
+
+
+
+
+ @if (Id != 0 && !isEditMode)
+ {
+
+ Edit
+
+ }
+
+
+
+ @if (isLoading)
+ {
+
+
+
+ Loading...
+
+
Loading customer...
+
+
+ }
+ else if (errorMessage != null)
+ {
+
+ }
+ else
+ {
+
+
+
+ @* General Section *@
+
+
+ @* Contact Section *@
+
+
+ @* Invoicing Section *@
+
+
+
+
+
+
+
+ Accounts Receivable
+
+
+
+ -- Select Account --
+ @foreach (var account in Accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+ Sales
+
+
+
+ -- Select Account --
+ @foreach (var account in Accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+ Prepayment
+
+
+
+ -- Select Account --
+ @foreach (var account in Accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+ Discount
+
+
+
+ -- Select Account --
+ @foreach (var account in Accounts)
+ {
+ @account.Text
+ }
+
+
+
+
+
+
+
+ Tax Group
+
+
+
+ -- Select Tax Group --
+ @foreach (var taxGroup in TaxGroups)
+ {
+ @taxGroup.Text
+ }
+
+
+
+
+
+
+
+
+ @* Payment Section *@
+
+
+
+
+
+
+
+ Payment Term
+
+
+
+ -- Select Payment Term --
+ @foreach (var term in PaymentTerms)
+ {
+ @term.Text
+ }
+
+
+
+
+
+
+
+
+
+
+ @if (isEditMode || Id == 0)
+ {
+
+ Save
+
+ }
+
+ Close
+
+
+
+
+ }
+
+
+@code {
+ [Parameter]
+ public int Id { get; set; } = 0;
+
+ private CustomerDto Model { get; set; } = new();
+
+ private List Accounts { get; set; } = new();
+ private List TaxGroups { get; set; } = new();
+ private List PaymentTerms { get; set; } = new();
+
+ private bool isLoading = true;
+ private bool isEditMode = false;
+ private string? errorMessage;
+
+ protected override async Task OnInitializedAsync()
+ {
+ isLoading = true;
+
+ if (Id != 0)
+ {
+ await LoadCustomer();
+ }
+ else
+ {
+ isEditMode = true;
+ await GenerateCustomerNumber();
+ }
+
+ await LoadDropdownData();
+
+ isLoading = false;
+ }
+
+ private async Task GenerateCustomerNumber()
+ {
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + "sales/customers";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var customers = await response.Content.ReadFromJsonAsync>();
+ if (customers != null && customers.Any())
+ {
+ // Find the highest customer number
+ var maxNo = customers
+ .Select(c => int.TryParse(c.No, out int num) ? num : 0)
+ .DefaultIfEmpty(0)
+ .Max();
+
+ Model.No = (maxNo + 1).ToString();
+ }
+ else
+ {
+ Model.No = "1";
+ }
+ }
+ }
+ catch (Exception)
+ {
+ Model.No = "1";
+ }
+ }
+
+ private async Task LoadCustomer()
+ {
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = $"{baseApiUrl}sales/customer?id={Id}";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Model = await response.Content.ReadFromJsonAsync() ?? new();
+ }
+ else
+ {
+ errorMessage = $"Failed to load customer. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading customer: {ex.Message}";
+ }
+ }
+
+ private async Task LoadDropdownData()
+ {
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ // Load accounts
+ var accountsResponse = await Http.GetAsync(baseApiUrl + "common/postingaccounts");
+ if (accountsResponse.IsSuccessStatusCode)
+ {
+ var accounts = await accountsResponse.Content.ReadFromJsonAsync>();
+ Accounts = accounts?.Select(a => new SelectListItem { Value = a.Id.ToString(), Text = a.AccountName! }).ToList() ?? new();
+ }
+
+ // Load tax groups
+ var taxGroupsResponse = await Http.GetAsync(baseApiUrl + "tax/taxgroups");
+ if (taxGroupsResponse.IsSuccessStatusCode)
+ {
+ var taxGroups = await taxGroupsResponse.Content.ReadFromJsonAsync>();
+ TaxGroups = taxGroups?.Select(tg => new SelectListItem { Value = tg.Id.ToString(), Text = tg.Description! }).ToList() ?? new();
+ }
+
+ // Load payment terms
+ var paymentTermsResponse = await Http.GetAsync(baseApiUrl + "common/paymentterms");
+ if (paymentTermsResponse.IsSuccessStatusCode)
+ {
+ var paymentTerms = await paymentTermsResponse.Content.ReadFromJsonAsync>();
+ PaymentTerms = paymentTerms?.Select(pt => new SelectListItem { Value = pt.Id.ToString(), Text = pt.Description }).ToList() ?? new();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading form data: {ex.Message}";
+ }
+ }
+
+ private async Task HandleValidSubmit()
+ {
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + "sales/savecustomer";
+
+ var response = await Http.PostAsJsonAsync(apiUrl, Model);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Navigation.NavigateTo("/sales/customers", forceLoad: true);
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save customer. Status: {response.StatusCode}. {errorContent}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving customer: {ex.Message}";
+ }
+ }
+
+ private void EnableEditMode()
+ {
+ isEditMode = true;
+ }
+
+ private void NavigateToCustomerList()
+ {
+ Navigation.NavigateTo("/sales/customers", forceLoad: true);
+ }
+
+ private void NavigateToAddContact()
+ {
+ Navigation.NavigateTo($"/contact/contact?partyId={Model.Id}&partyType=1", forceLoad: true);
+ }
+
+ private void NavigateToContacts()
+ {
+ Navigation.NavigateTo($"/contact/contacts?partyId={Model.Id}&partyType=1", forceLoad: true);
+ }
+
+ public class SelectListItem
+ {
+ public string Value { get; set; } = string.Empty;
+ public string Text { get; set; } = string.Empty;
+ }
+
+ public class PaymentTermResponse
+ {
+ public int Id { get; set; }
+ public string Description { get; set; } = string.Empty;
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Sales/Customers.razor b/src/AccountGoWeb/Components/Pages/Sales/Customers.razor
new file mode 100644
index 000000000..fbac6de14
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/Customers.razor
@@ -0,0 +1,224 @@
+@using CustomerDto = Dto.Sales.Customer
+@inject HttpClient Http
+@inject NavigationManager Navigation
+@rendermode InteractiveServer
+
+Customers
+
+
+
+
+
+
+ New Customer
+
+ @if (selectedCustomer != null)
+ {
+
+ View
+
+ }
+
+
+
+ @if (isLoading)
+ {
+
+
+
+ Loading...
+
+
Loading customers...
+
+
+ }
+ else if (errorMessage != null)
+ {
+
+ }
+ else if (customers == null || !customers.Any())
+ {
+
+
+
+ No customers found.
+
+
+
+ }
+ else
+ {
+
+
+
+
+
+
+ SortBy(nameof(CustomerDto.No))" style="cursor: pointer;">
+ No @GetSortIcon(nameof(CustomerDto.No))
+
+ SortBy(nameof(CustomerDto.Name))" style="cursor: pointer;">
+ Name @GetSortIcon(nameof(CustomerDto.Name))
+
+ SortBy(nameof(CustomerDto.Phone))" style="cursor: pointer;">
+ Phone @GetSortIcon(nameof(CustomerDto.Phone))
+
+ SortBy(nameof(CustomerDto.Contact))" style="cursor: pointer;">
+ Contact @GetSortIcon(nameof(CustomerDto.Contact))
+
+ SortBy(nameof(CustomerDto.TaxGroup))" style="cursor: pointer;">
+ Tax Group @GetSortIcon(nameof(CustomerDto.TaxGroup))
+
+ SortBy(nameof(CustomerDto.Balance))" style="cursor: pointer;">
+ Balance @GetSortIcon(nameof(CustomerDto.Balance))
+
+
+
+
+ @foreach (var customer in customers)
+ {
+ SelectCustomer(customer)"
+ style="cursor: pointer; @(selectedCustomer?.Id == customer.Id ? "background-color: #007bff33; border-left: 3px solid #007bff;" : "")">
+
+
+ @customer.No
+
+
+ @customer.Name
+ @customer.Phone
+ @customer.Contact
+ @customer.TaxGroup
+ @customer.Balance.ToString("N2")
+
+ }
+
+
+
+
+
+
+
+
+
Total: @customers.Count() customer(s)
+
+
+ }
+
+
+@code {
+ private List? customers;
+ private List? allCustomers;
+ private CustomerDto? selectedCustomer;
+ private bool isLoading = true;
+ private string? errorMessage;
+ private string? sortColumn;
+ private bool sortAscending = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadCustomers();
+ }
+
+ private async Task LoadCustomers()
+ {
+ isLoading = true;
+ errorMessage = null;
+
+ try
+ {
+ string baseApiUrl = Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var apiUrl = baseApiUrl + "sales/customers";
+ var response = await Http.GetAsync(apiUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ allCustomers = await response.Content.ReadFromJsonAsync>();
+ customers = allCustomers;
+ }
+ else
+ {
+ errorMessage = $"Failed to load customers. Status: {response.StatusCode}";
+ }
+ }
+ catch (HttpRequestException ex)
+ {
+ errorMessage = $"Network error: {ex.Message}. Please ensure the API is running on port 8001.";
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading customers: {ex.Message}";
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SelectCustomer(CustomerDto customer)
+ {
+ selectedCustomer = customer;
+ }
+
+ private void NavigateToNewCustomer()
+ {
+ Navigation.NavigateTo("/sales/customer", forceLoad: true);
+ }
+
+ private void NavigateToViewCustomer()
+ {
+ if (selectedCustomer != null)
+ {
+ Navigation.NavigateTo($"/sales/customer/{selectedCustomer.Id}", forceLoad: true);
+ }
+ }
+
+ private void SortBy(string column)
+ {
+ if (customers == null) return;
+
+ if (sortColumn == column)
+ {
+ sortAscending = !sortAscending;
+ }
+ else
+ {
+ sortColumn = column;
+ sortAscending = true;
+ }
+
+ customers = column switch
+ {
+ nameof(CustomerDto.No) => sortAscending
+ ? customers.OrderBy(c => c.No).ToList()
+ : customers.OrderByDescending(c => c.No).ToList(),
+ nameof(CustomerDto.Name) => sortAscending
+ ? customers.OrderBy(c => c.Name).ToList()
+ : customers.OrderByDescending(c => c.Name).ToList(),
+ nameof(CustomerDto.Phone) => sortAscending
+ ? customers.OrderBy(c => c.Phone).ToList()
+ : customers.OrderByDescending(c => c.Phone).ToList(),
+ nameof(CustomerDto.Contact) => sortAscending
+ ? customers.OrderBy(c => c.Contact).ToList()
+ : customers.OrderByDescending(c => c.Contact).ToList(),
+ nameof(CustomerDto.TaxGroup) => sortAscending
+ ? customers.OrderBy(c => c.TaxGroup).ToList()
+ : customers.OrderByDescending(c => c.TaxGroup).ToList(),
+ nameof(CustomerDto.Balance) => sortAscending
+ ? customers.OrderBy(c => c.Balance).ToList()
+ : customers.OrderByDescending(c => c.Balance).ToList(),
+ _ => customers
+ };
+ }
+
+ private string GetSortIcon(string column)
+ {
+ if (sortColumn != column) return "";
+ return sortAscending ? "▲" : "▼";
+ }
+}
diff --git a/src/AccountGoWeb/Components/Pages/Sales/NewSalesOrder.razor b/src/AccountGoWeb/Components/Pages/Sales/NewSalesOrder.razor
new file mode 100644
index 000000000..23ca2b800
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/NewSalesOrder.razor
@@ -0,0 +1,461 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+
+
+
+
+
+
+
+
+ @if (!isNew)
+ {
+ editMode = !editMode">
+ @(editMode ? "Cancel" : "Edit")
+
+ }
+
+
+
+ @if (loading)
+ {
+
Loading...
+ }
+ else if (errorMessage != null)
+ {
+
+ Error: @errorMessage
+
+ }
+ else if (order == null)
+ {
+
Sales order not found.
+ }
+ else
+ {
+
+
+
+
+
+
+ Customer
+
+ -- Select Customer --
+ @foreach (var customer in customers)
+ {
+ @customer.name
+ }
+
+
+
+ Payment Term
+
+ -- Select Payment Term --
+ @foreach (var term in paymentTerms)
+ {
+ @term.description
+ }
+
+
+
+
+
+
+
Line Items
+
+
+ @if (editMode)
+ {
+
Add Line Item
+ }
+
+
+
+
+ @if (editMode)
+ {
+
Save Order
+
Cancel
+ }
+
+
+
+ }
+
+
+@code {
+ private class LineItem
+ {
+ public int ItemId { get; set; } = 1;
+ public string ItemDescription { get; set; } = "";
+ public int MeasurementId { get; set; } = 1;
+ public decimal Quantity { get; set; } = 1;
+ public decimal Amount { get; set; } = 0;
+ public decimal Discount { get; set; } = 0;
+ }
+
+ private class Customer
+ {
+ public int id { get; set; }
+ public string name { get; set; } = "";
+ }
+
+ private class Item
+ {
+ public int id { get; set; }
+ public string description { get; set; } = "";
+ }
+
+ private class PaymentTerm
+ {
+ public int id { get; set; }
+ public string description { get; set; } = "";
+ }
+
+ private class Measurement
+ {
+ public int id { get; set; }
+ public string description { get; set; } = "";
+ }
+
+ [Parameter]
+ public string Id { get; set; } = null!;
+
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ private Dictionary order = new();
+ private bool loading = true;
+ private bool editMode = false;
+ private bool isNew = false;
+ private string? errorMessage = null;
+
+ private string orderNo = "";
+ private int customerId = 1;
+ private string customerName = "";
+ private DateTime orderDate = DateTime.Now;
+ private string referenceNo = "";
+ private int paymentTermId = 1;
+ private decimal totalAmount = 0;
+ private List lineItems = new();
+
+ private List customers = new();
+ private List- items = new();
+ private List
paymentTerms = new();
+ private List measurements = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadCustomers();
+ await LoadItems();
+ await LoadPaymentTerms();
+ await LoadMeasurements();
+
+ isNew = Id == "new" || string.IsNullOrEmpty(Id);
+
+ if (isNew)
+ {
+ order = new Dictionary();
+ orderNo = new Random().Next(1, 99999).ToString();
+ orderDate = DateTime.Now;
+ lineItems = new List { new LineItem() };
+ editMode = true;
+ loading = false;
+ }
+ else
+ {
+ Navigation.NavigateTo("/Sales/AddSalesOrder");
+ }
+ }
+
+ private async Task LoadCustomers()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}sales/customers");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ customers = JsonSerializer.Deserialize>(responseString) ?? new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading customers: {ex.Message}";
+ }
+ }
+
+ private async Task LoadItems()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}inventory/items");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ items = JsonSerializer.Deserialize>(responseString) ?? new List- ();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading items: {ex.Message}";
+ }
+ }
+
+ private async Task LoadPaymentTerms()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}common/paymentterms");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ paymentTerms = JsonSerializer.Deserialize
>(responseString) ?? new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading payment terms: {ex.Message}";
+ }
+ }
+
+ private async Task LoadMeasurements()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}common/measurements");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ measurements = JsonSerializer.Deserialize>(responseString) ?? new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading measurements: {ex.Message}";
+ }
+ }
+
+ private void MapJsonToProperties(System.Text.Json.JsonElement data)
+ {
+ orderNo = GetJsonProperty(data, "no");
+ customerName = GetJsonProperty(data, "customerName");
+ referenceNo = GetJsonProperty(data, "referenceNo");
+
+ if (data.TryGetProperty("orderDate", out var dateElem))
+ {
+ if (DateTime.TryParse(dateElem.GetString(), out var date))
+ orderDate = date;
+ }
+
+ lineItems = new();
+ if (data.TryGetProperty("salesOrderLines", out var linesElem) && linesElem.ValueKind ==
+ System.Text.Json.JsonValueKind.Array)
+ {
+ foreach (var lineElem in linesElem.EnumerateArray())
+ {
+ var line = new LineItem
+ {
+ ItemDescription = GetJsonProperty(lineElem, "itemDescription"),
+ Quantity = GetJsonPropertyDecimal(lineElem, "quantity"),
+ Amount = GetJsonPropertyDecimal(lineElem, "amount"),
+ Discount = GetJsonPropertyDecimal(lineElem, "discount")
+ };
+ lineItems.Add(line);
+ }
+ }
+
+ CalculateTotalAmount();
+ }
+
+ private string GetJsonProperty(System.Text.Json.JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ return prop.GetString() ?? "";
+ }
+ return "";
+ }
+
+ private decimal GetJsonPropertyDecimal(System.Text.Json.JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.TryGetDecimal(out var val))
+ return val;
+ }
+ return 0;
+ }
+
+ private void AddLineItem()
+ {
+ lineItems.Add(new LineItem());
+ CalculateTotalAmount();
+ StateHasChanged();
+ }
+
+ private void RemoveLineItem(int index)
+ {
+ if (index >= 0 && index < lineItems.Count)
+ {
+ lineItems.RemoveAt(index);
+ }
+ CalculateTotalAmount();
+ StateHasChanged();
+ }
+
+ private void CalculateTotalAmount()
+ {
+ totalAmount = lineItems.Sum(line =>
+ {
+ var qty = line.Quantity;
+ var amt = line.Amount;
+ var disc = line.Discount;
+ var total = qty * amt;
+ var discount = (disc / 100) * total;
+ return total - discount;
+ });
+ }
+
+ private async Task SaveSalesOrder()
+ {
+ try
+ {
+ CalculateTotalAmount();
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var payload = new
+ {
+ CustomerId = customerId,
+ OrderDate = orderDate,
+ PaymentTermId = paymentTermId,
+ ReferenceNo = referenceNo,
+ SalesOrderLines = lineItems.Select(line => new
+ {
+ ItemId = line.ItemId,
+ MeasurementId = line.MeasurementId,
+ Quantity = line.Quantity,
+ Amount = line.Amount,
+ Discount = line.Discount
+ }).ToList()
+ };
+
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(payload);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync($"{apiurl}sales/addsalesorder", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ editMode = false;
+ errorMessage = null;
+ await Task.Delay(100);
+ Navigation.NavigateTo("/Sales/SalesOrders", forceLoad: true);
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save sales order. Status: {response.StatusCode}. Response: {errorContent}";
+ StateHasChanged();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving sales order: {ex.Message}";
+ StateHasChanged();
+ }
+ }
+
+ private void CancelSalesOrder()
+ {
+ Navigation.NavigateTo("/Sales/SalesOrders");
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/SalesInvoice.razor b/src/AccountGoWeb/Components/Pages/Sales/SalesInvoice.razor
new file mode 100644
index 000000000..2e805a788
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/SalesInvoice.razor
@@ -0,0 +1,471 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+
+
+
+
+
+
+ @if (loading)
+ {
+
Loading invoice...
+ }
+ else if (!string.IsNullOrEmpty(errorMessage))
+ {
+
+ Error: @errorMessage
+
+ }
+ else
+ {
+
+
+
+
+
+
+
+
+ Customer
+
+ -- Select Customer --
+ @foreach (var customer in customers)
+ {
+ @customer.name
+ }
+
+
+
+ Payment Term
+
+ -- Select Payment Term --
+ @foreach (var term in paymentTerms)
+ {
+ @term.description
+ }
+
+
+
+
+
+
+
Line Items
+
+
+
+
+
+ Add Row
+
+
+
+
+
+
+
+
+ }
+
+
+@code {
+ private class LineItem
+ {
+ public int? ItemId { get; set; } = 0;
+ public decimal? Quantity { get; set; } = 0;
+ public decimal? Amount { get; set; } = 0;
+ public decimal? Discount { get; set; } = 0;
+ public int? MeasurementId { get; set; } = 0;
+ }
+
+ private class Customer
+ {
+ public int id { get; set; }
+ public string name { get; set; } = "";
+ }
+
+ private class Item
+ {
+ public int id { get; set; }
+ public string description { get; set; } = "";
+ }
+
+ private class PaymentTerm
+ {
+ public int id { get; set; }
+ public string description { get; set; } = "";
+ }
+
+ private class Measurement
+ {
+ public int id { get; set; }
+ public string description { get; set; } = "";
+ }
+
+ [SupplyParameterFromQuery(Name = "id")]
+ public int Id { get; set; }
+
+ [SupplyParameterFromQuery(Name = "orderId")]
+ public int? OrderId { get; set; }
+
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ private bool loading = true;
+ private string? errorMessage = null;
+
+ private int invoiceId = 0;
+ private DateTime invoiceDate = DateTime.Now;
+ private int customerId = 0;
+ private int paymentTermId = 0;
+ private decimal totalAmount = 0;
+ private List lineItems = new();
+
+ private List customers = new();
+ private List- items = new();
+ private List
paymentTerms = new();
+ private List measurements = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Use OrderId if provided, otherwise use Id
+ var finalId = OrderId ?? Id;
+
+ await LoadCustomers();
+ await LoadItems();
+ await LoadPaymentTerms();
+ await LoadMeasurements();
+ await LoadInvoiceData(finalId);
+ }
+
+ private async Task LoadCustomers()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}sales/customers");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ customers = JsonSerializer.Deserialize>(responseString) ?? new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading customers: {ex.Message}";
+ }
+ }
+
+ private async Task LoadItems()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}inventory/items");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ items = JsonSerializer.Deserialize>(responseString) ?? new List- ();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading items: {ex.Message}";
+ }
+ }
+
+ private async Task LoadPaymentTerms()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}common/paymentterms");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ paymentTerms = JsonSerializer.Deserialize
>(responseString) ?? new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading payment terms: {ex.Message}";
+ }
+ }
+
+ private async Task LoadMeasurements()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}common/measurements");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ measurements = JsonSerializer.Deserialize>(responseString) ?? new List();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading measurements: {ex.Message}";
+ }
+ }
+
+ private async Task LoadInvoiceData(int saleOrderId)
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}sales/SalesInvoice?id={saleOrderId}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ var invoiceData = JsonSerializer.Deserialize(responseString, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ if (invoiceData.ValueKind == JsonValueKind.Object)
+ {
+ // Parse invoice data
+ if (invoiceData.TryGetProperty("id", out var idElem))
+ invoiceId = idElem.GetInt32();
+
+ if (invoiceData.TryGetProperty("customerId", out var custElem))
+ customerId = custElem.GetInt32();
+
+ if (invoiceData.TryGetProperty("invoiceDate", out var dateElem))
+ {
+ if (DateTime.TryParse(dateElem.GetString(), out var date))
+ invoiceDate = date;
+ }
+
+ if (invoiceData.TryGetProperty("paymentTermId", out var termElem))
+ paymentTermId = termElem.GetInt32();
+
+ // Parse line items
+ if (invoiceData.TryGetProperty("salesInvoiceLines", out var linesElem) && linesElem.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var lineElem in linesElem.EnumerateArray())
+ {
+ var line = new LineItem
+ {
+ ItemId = GetJsonInt(lineElem, "itemId"),
+ Quantity = GetJsonDecimal(lineElem, "quantity"),
+ Amount = GetJsonDecimal(lineElem, "amount"),
+ Discount = GetJsonDecimal(lineElem, "discount"),
+ MeasurementId = GetJsonInt(lineElem, "measurementId")
+ };
+ lineItems.Add(line);
+ }
+ }
+ else if (saleOrderId == 0)
+ {
+ // For new invoices, add 1 empty row matching original implementation
+ lineItems.Add(new LineItem { ItemId = 1, Quantity = 1, Amount = 0, Discount = 0, MeasurementId = 1 });
+ }
+
+ CalculateTotalAmount();
+ }
+ }
+ else
+ {
+ errorMessage = $"Failed to load invoice. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading invoice: {ex.Message}";
+ }
+ finally
+ {
+ loading = false;
+ StateHasChanged();
+ }
+ }
+
+ private int? GetJsonInt(JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.TryGetInt32(out var val))
+ return val;
+ }
+ return null;
+ }
+
+ private decimal? GetJsonDecimal(JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.TryGetDecimal(out var val))
+ return val;
+ }
+ return null;
+ }
+
+ private void CalculateTotalAmount()
+ {
+ totalAmount = lineItems.Sum(line =>
+ {
+ var qty = line.Quantity ?? 0;
+ var amt = line.Amount ?? 0;
+ var disc = line.Discount ?? 0;
+ var total = qty * amt;
+ var discount = (disc / 100) * total;
+ return total - discount;
+ });
+ }
+
+ private void AddLineItem()
+ {
+ lineItems.Add(new LineItem { ItemId = 1, Quantity = 1, Amount = 0, Discount = 0, MeasurementId = 1 });
+ StateHasChanged();
+ }
+
+ private async Task SaveSalesInvoice()
+ {
+ try
+ {
+ CalculateTotalAmount();
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ // Use OrderId if provided, otherwise use Id
+ var finalId = OrderId ?? Id;
+
+ var payload = new
+ {
+ Id = invoiceId,
+ CustomerId = customerId,
+ InvoiceDate = invoiceDate,
+ PaymentTermId = paymentTermId,
+ FromSalesOrderId = finalId, // The sales order this invoice is being created from
+ SalesInvoiceLines = lineItems.Select(line => new
+ {
+ ItemId = line.ItemId,
+ Quantity = line.Quantity,
+ Amount = line.Amount,
+ Discount = line.Discount,
+ MeasurementId = line.MeasurementId
+ }).ToList()
+ };
+
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(payload);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync($"{apiurl}sales/SaveSalesInvoice", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ await Task.Delay(100);
+ Navigation.NavigateTo("/Sales/SalesInvoices", forceLoad: true);
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save invoice. Status: {response.StatusCode}. Response: {errorContent}";
+ StateHasChanged();
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving invoice: {ex.Message}";
+ StateHasChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/SalesOrder.razor b/src/AccountGoWeb/Components/Pages/Sales/SalesOrder.razor
new file mode 100644
index 000000000..ab17467a0
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/SalesOrder.razor
@@ -0,0 +1,313 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@using Dto.Sales
+
+
+
+
+
+
+ @if (loading)
+ {
+
Loading...
+ }
+ else if (errorMessage != null)
+ {
+
+ Error: @errorMessage
+
+ }
+ else if (!HasSalesOrder())
+ {
+
+ Sales order not found.
+
+ }
+ else
+ {
+
+
+
+
+
+
+
+
Customer Name
+
+ @GetValue("customerName")
+
+
+
Order Date
+
+ @GetValue("orderDate")
+
+
+
Amount
+
+ @GetTotalAmount().ToString("F2")
+
+
+
+
+
Order Items
+
+
+
+
+ Item
+ Quantity
+ Amount
+ Discount
+ Measurement
+
+
+
+ @if (salesOrderLines != null && salesOrderLines.Count > 0)
+ {
+ @foreach (var line in salesOrderLines)
+ {
+
+ @GetLineValue(line, "itemDescription")
+ @GetLineValue(line, "quantity")
+ @GetLineValue(line, "amount")
+ @GetLineValue(line, "discount")
+ @GetLineValue(line, "measurementDescription")
+
+ }
+ }
+
+
+
+
+
+
+ }
+
+
+@code {
+ [SupplyParameterFromQuery]
+ public int Id { get; set; }
+
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ private System.Text.Json.JsonElement? salesOrder;
+ private List salesOrderLines = new();
+ private bool loading = true;
+ private string? errorMessage = null;
+ private string apiResponse = "No response yet";
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadSalesOrder();
+ }
+
+ private bool HasSalesOrder()
+ {
+ return salesOrder.HasValue && salesOrder.Value.ValueKind != System.Text.Json.JsonValueKind.Null;
+ }
+
+ private async Task LoadSalesOrder()
+ {
+ try
+ {
+ loading = true;
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60);
+
+ var fullUrl = $"{apiurl}sales/SalesOrder?id={Id}";
+ var response = await client.GetAsync(fullUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ apiResponse = responseString;
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ // Check if response is an exception (has "ClassName" property)
+ using (var doc = System.Text.Json.JsonDocument.Parse(responseString))
+ {
+ if (doc.RootElement.TryGetProperty("ClassName", out var classNameProp))
+ {
+ // This is an exception response
+ var className = classNameProp.GetString() ?? "Unknown Error";
+ if (className.Contains("InvalidOperationException") &&
+ doc.RootElement.TryGetProperty("Message", out var messageProp) &&
+ messageProp.GetString()?.Contains("Nullable object") == true)
+ {
+ errorMessage = "This sales order is incomplete or missing required data (e.g., customer information). Please edit the order to complete it.";
+ }
+ else if (doc.RootElement.TryGetProperty("Message", out var msgProp))
+ {
+ errorMessage = $"API Error: {msgProp.GetString()}";
+ }
+ else
+ {
+ errorMessage = "API Error: Unknown error occurred";
+ }
+ loading = false;
+ return;
+ }
+ }
+
+ var doc2 = System.Text.Json.JsonDocument.Parse(responseString);
+ salesOrder = doc2.RootElement;
+
+ // Extract sales order lines - try both cases
+ if (salesOrder.Value.TryGetProperty("salesOrderLines", out var linesProperty) ||
+ salesOrder.Value.TryGetProperty("SalesOrderLines", out linesProperty))
+ {
+ var linesList = JsonSerializer.Deserialize>(linesProperty.GetRawText(), options);
+ salesOrderLines = linesList?.Cast().ToList() ?? new();
+ }
+ }
+ else
+ {
+ var content = await response.Content.ReadAsStringAsync();
+ apiResponse = content;
+ errorMessage = $"Failed to load sales order. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ apiResponse = ex.ToString();
+ errorMessage = $"Error: {ex.Message}";
+ }
+ finally
+ {
+ loading = false;
+ }
+ }
+
+ private string GetValue(string propertyName)
+ {
+ try
+ {
+ if (salesOrder.HasValue && salesOrder.Value is System.Text.Json.JsonElement je)
+ {
+ if (je.TryGetProperty(propertyName, out var prop))
+ {
+ return FormatValue(prop, propertyName);
+ }
+
+ // Try case-insensitive
+ foreach (var objProp in je.EnumerateObject())
+ {
+ if (objProp.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
+ {
+ return FormatValue(objProp.Value, propertyName);
+ }
+ }
+ }
+ return "N/A";
+ }
+ catch
+ {
+ return "N/A";
+ }
+ }
+
+ private string GetLineValue(dynamic obj, string propertyName)
+ {
+ try
+ {
+ if (obj is System.Text.Json.JsonElement je)
+ {
+ if (je.TryGetProperty(propertyName, out var prop))
+ {
+ return FormatValue(prop, propertyName);
+ }
+
+ // Try case-insensitive
+ foreach (var objProp in je.EnumerateObject())
+ {
+ if (objProp.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
+ {
+ return FormatValue(objProp.Value, propertyName);
+ }
+ }
+ }
+ return "N/A";
+ }
+ catch
+ {
+ return "N/A";
+ }
+ }
+
+ private string FormatValue(System.Text.Json.JsonElement prop, string propertyName)
+ {
+ if ((propertyName == "amount" || propertyName == "discount") && prop.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ return prop.GetDecimal().ToString("F2");
+ }
+ if (propertyName == "quantity" && prop.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ return prop.GetDecimal().ToString("F2");
+ }
+ if (propertyName == "orderDate" && prop.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ var dateStr = prop.GetString();
+ return dateStr?.Substring(0, Math.Min(10, dateStr.Length)) ?? "N/A";
+ }
+ if (prop.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ return prop.GetString() ?? "N/A";
+ }
+ if (prop.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ return prop.GetDecimal().ToString();
+ }
+ return prop.ToString();
+ }
+
+ private decimal GetTotalAmount()
+ {
+ decimal total = 0;
+ if (salesOrderLines != null && salesOrderLines.Count > 0)
+ {
+ foreach (var line in salesOrderLines)
+ {
+ if (line is System.Text.Json.JsonElement je)
+ {
+ if (je.TryGetProperty("amount", out var amountProp) && amountProp.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ total += amountProp.GetDecimal();
+ }
+ }
+ }
+ }
+ return total;
+ }
+
+ private void OnClickEditButton()
+ {
+ Navigation.NavigateTo($"/Sales/AddSalesOrder?id={Id}");
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/SalesOrders.razor b/src/AccountGoWeb/Components/Pages/Sales/SalesOrders.razor
new file mode 100644
index 000000000..3986026a8
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/SalesOrders.razor
@@ -0,0 +1,321 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+
+
+
+
+
+
+ @* SALES ORDER LIST *@
+
+ @if (loading)
+ {
+
Loading...
+ }
+ else if (errorMessage != null)
+ {
+
+ Error: @errorMessage
+
+ }
+ else if (salesOrders.Count == 0)
+ {
+
+ No sales orders found.
+
+ }
+ else
+ {
+
+
+
+
+ No
+ Customer Name
+ Order Date
+ Ref no
+ Amount
+ Status
+
+
+
+ @if (salesOrders.Count > 0)
+ {
+ @for (int i = 0; i < salesOrders.Count; i++)
+ {
+ int index = i;
+ OnRowSelected(index))">
+ @GetValue(salesOrders[index], "no")
+ @GetValue(salesOrders[index], "customerName")
+ @GetValue(salesOrders[index], "orderDate")
+ @GetValue(salesOrders[index], "referenceNo")
+ @GetValue(salesOrders[index], "amount")
+ @GetValue(salesOrders[index], "status")
+
+ }
+ }
+
+
+
+ }
+
+
+@code {
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ private List salesOrders = new();
+ private bool loading = true;
+ private string? errorMessage = null;
+ private bool shouldRefresh = false;
+ private string? selectedOrderId = null;
+ private int selectedRowIndex = -1;
+ private bool isInvoiceDisabled = false;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadSalesOrders();
+ Navigation.LocationChanged += OnLocationChanged;
+ }
+
+ private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
+ {
+ // Refresh data when navigating back to this page
+ if (e.Location.Contains("/sales/sales-orders"))
+ {
+ shouldRefresh = true;
+ InvokeAsync(StateHasChanged);
+ }
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (shouldRefresh && !firstRender)
+ {
+ shouldRefresh = false;
+ await LoadSalesOrders();
+ }
+ }
+
+ public void Dispose()
+ {
+ Navigation.LocationChanged -= OnLocationChanged;
+ }
+
+ private async Task LoadSalesOrders()
+ {
+ try
+ {
+ loading = true;
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60); // Increase timeout to 60 seconds
+
+ var response = await client.GetAsync($"{apiurl}sales/salesorders");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ var data = JsonSerializer.Deserialize>(responseString, options);
+ salesOrders = data?.Cast().ToList() ?? new();
+ }
+ else
+ {
+ errorMessage = $"Failed to load sales orders. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading sales orders: {ex.Message}";
+ }
+ finally
+ {
+ loading = false;
+ }
+ }
+
+ private string GetValue(dynamic obj, string propertyName)
+ {
+ try
+ {
+ if (obj is System.Text.Json.JsonElement je)
+ {
+ if (je.TryGetProperty(propertyName, out var prop))
+ {
+ if (propertyName == "amount" && prop.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ return prop.GetDecimal().ToString("F2");
+ }
+ return prop.GetString() ?? prop.ToString();
+ }
+ }
+ return "N/A";
+ }
+ catch
+ {
+ return "N/A";
+ }
+ }
+
+ private void OnRowSelected(int index)
+ {
+ // Toggle selection - if clicking the same row, deselect it
+ if (selectedRowIndex == index)
+ {
+ selectedRowIndex = -1;
+ selectedOrderId = null;
+ isInvoiceDisabled = false;
+ }
+ else
+ {
+ selectedRowIndex = index;
+
+ try
+ {
+ if (index >= 0 && index < salesOrders.Count)
+ {
+ var order = salesOrders[index];
+ if (order is System.Text.Json.JsonElement je)
+ {
+ // Try to get ID - it might be a number or string
+ string? orderId = null;
+
+ if (je.TryGetProperty("id", out var idProp))
+ {
+ // Handle both string and numeric IDs
+ if (idProp.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ orderId = idProp.GetString();
+ }
+ else if (idProp.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ orderId = idProp.GetInt32().ToString();
+ }
+ }
+ else if (je.TryGetProperty("Id", out var idProp2))
+ {
+ if (idProp2.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ orderId = idProp2.GetString();
+ }
+ else if (idProp2.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ orderId = idProp2.GetInt32().ToString();
+ }
+ }
+ else if (je.TryGetProperty("salesOrderId", out var soIdProp))
+ {
+ if (soIdProp.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ orderId = soIdProp.GetString();
+ }
+ else if (soIdProp.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ orderId = soIdProp.GetInt32().ToString();
+ }
+ }
+
+ if (!string.IsNullOrEmpty(orderId))
+ {
+ selectedOrderId = orderId;
+
+ // Check if status is 6 (Fully Invoiced)
+ isInvoiceDisabled = false;
+ if (je.TryGetProperty("statusId", out var statusProp))
+ {
+ try
+ {
+ if (statusProp.ValueKind == System.Text.Json.JsonValueKind.Number && statusProp.GetInt32() == 6)
+ {
+ isInvoiceDisabled = true;
+ }
+ }
+ catch
+ {
+ // Status might not be an int, skip
+ }
+ }
+ }
+ }
+ }
+ }
+ catch
+ {
+ selectedOrderId = null;
+ }
+ }
+
+ StateHasChanged();
+ }
+ private void UpdateButtonLinks()
+ {
+ StateHasChanged();
+ }
+
+ private string GetViewOrderLink()
+ {
+ return selectedOrderId != null ? $"/Sales/SalesOrder?id={selectedOrderId}" : "javascript:void(0)";
+ }
+
+ private string GetInvoiceLink()
+ {
+ return selectedOrderId != null ? $"/Sales/SalesInvoice?orderId={selectedOrderId}" : "javascript:void(0)";
+ }
+
+ private string GetRawJson(int index)
+ {
+ if (index >= 0 && index < salesOrders.Count)
+ {
+ var order = salesOrders[index];
+ if (order is System.Text.Json.JsonElement je)
+ {
+ return je.GetRawText();
+ }
+ }
+ return "{}";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/SalesReceipt.razor b/src/AccountGoWeb/Components/Pages/Sales/SalesReceipt.razor
new file mode 100644
index 000000000..56492b8a7
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/SalesReceipt.razor
@@ -0,0 +1,456 @@
+@using System.Text.Json
+@using System.Text.Json.Serialization
+@using AccountGoWeb.Models
+@using AccountGoWeb.Models.Sales
+@using Microsoft.AspNetCore.Components
+
+
+
+
+
+
+
Sales Receipt
+
+
+ @if (!isEditing)
+ {
+
+
+ Edit
+
+ }
+ else
+ {
+
+
+ Cancel
+
+ }
+
+
+ Back to Receipts
+
+
+
+
+ @if (loading)
+ {
+
Loading receipt...
+ }
+ else if (!string.IsNullOrEmpty(errorMessage))
+ {
+
+ Error: @errorMessage
+
+ }
+ else
+ {
+
+ }
+
+
+@code {
+ private bool loading = true;
+ private bool isEditing = false;
+ private string errorMessage = "";
+ private int receiptId = 0;
+ private string receiptNo = "";
+ private int customerId = 0;
+ private int originalCustomerId = 0;
+ private DateTime receiptDate = DateTime.Now;
+ private DateTime originalReceiptDate = DateTime.Now;
+ private int accountToDebitId = 0;
+ private int originalAccountToDebitId = 0;
+ private int accountToCreditId = 0;
+ private int originalAccountToCreditId = 0;
+ private decimal amount = 0;
+ private decimal originalAmount = 0;
+
+ private List<(string Text, string Value)> customers = new();
+ private List<(string Text, string Value)> debitAccounts = new();
+ private List<(string Text, string Value)> creditAccounts = new();
+ private Dictionary customerAdvanceAccounts = new();
+ private Dictionary accountNames = new();
+
+ [SupplyParameterFromQuery(Name = "id")]
+ public int Id { get; set; }
+
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60);
+
+ // Load customers from API
+ var customersResponse = await client.GetAsync($"{apiurl}sales/customers");
+ if (customersResponse.IsSuccessStatusCode)
+ {
+ var json = await customersResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var customersData = JsonSerializer.Deserialize>(json, options);
+ if (customersData != null)
+ {
+ foreach (var customer in customersData)
+ {
+ var id = GetJsonPropertyString(customer, "id");
+ var name = GetJsonPropertyString(customer, "name");
+ var prepaymentAccountId = GetJsonPropertyString(customer, "prepaymentAccountId");
+ customers.Add((name ?? "Unknown", id));
+
+ // Store the prepayment account ID for this customer
+ if (int.TryParse(id, out var customerId_int) && int.TryParse(prepaymentAccountId, out var accountId_int))
+ {
+ customerAdvanceAccounts[customerId_int] = accountId_int;
+ }
+ }
+ }
+ }
+
+ // Load debit accounts from API (Cash & Banks)
+ var debitResponse = await client.GetAsync($"{apiurl}financials/CashBanks");
+ if (debitResponse.IsSuccessStatusCode)
+ {
+ var json = await debitResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var accountsData = JsonSerializer.Deserialize>(json, options);
+ if (accountsData != null)
+ {
+ foreach (var account in accountsData)
+ {
+ var id = GetJsonPropertyString(account, "id");
+ var name = GetJsonPropertyString(account, "name");
+ debitAccounts.Add((name ?? "Unknown", id));
+ }
+ }
+ }
+
+ // Load credit accounts from API (posting accounts only)
+ var creditResponse = await client.GetAsync($"{apiurl}common/postingaccounts");
+ if (creditResponse.IsSuccessStatusCode)
+ {
+ var json = await creditResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var accountsData = JsonSerializer.Deserialize>(json, options);
+ if (accountsData != null)
+ {
+ foreach (var account in accountsData)
+ {
+ var id = GetJsonPropertyString(account, "id");
+ var name = GetJsonPropertyString(account, "accountName");
+ if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(name))
+ {
+ creditAccounts.Add((name, id));
+ // Store account name for display
+ if (int.TryParse(id, out var accountId))
+ {
+ accountNames[accountId] = name;
+ }
+ }
+ }
+ }
+ }
+
+ // Load all accounts to get customer advance account names
+ var allAccountsResponse = await client.GetAsync($"{apiurl}financials/accounts");
+ if (allAccountsResponse.IsSuccessStatusCode)
+ {
+ var json = await allAccountsResponse.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var accountsData = JsonSerializer.Deserialize>(json, options);
+ if (accountsData != null)
+ {
+ foreach (var account in accountsData)
+ {
+ var id = GetJsonPropertyString(account, "id");
+ var name = GetJsonPropertyString(account, "name");
+ if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(name))
+ {
+ // Store account name for display
+ if (int.TryParse(id, out var accountId))
+ {
+ accountNames[accountId] = name;
+ }
+ }
+ }
+ }
+ }
+
+ // Load receipt data if editing
+ if (Id > 0)
+ {
+ await LoadReceipt();
+ }
+
+ loading = false;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Failed to load form data: {ex.Message}";
+ loading = false;
+ }
+ }
+
+ private async Task LoadReceipt()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+ var client = ClientFactory.CreateClient();
+ var response = await client.GetAsync($"{apiurl}sales/salesreceipt?id={Id}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync();
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var receiptData = JsonSerializer.Deserialize(json, options);
+
+ if (receiptData.ValueKind == JsonValueKind.Object)
+ {
+ receiptId = GetJsonInt(receiptData, "id") ?? 0;
+ receiptNo = GetJsonPropertyString(receiptData, "receiptNo");
+ customerId = GetJsonInt(receiptData, "customerId") ?? 0;
+ accountToDebitId = GetJsonInt(receiptData, "accountToDebitId") ?? 0;
+ accountToCreditId = GetJsonInt(receiptData, "accountToCreditId") ?? 0;
+ amount = GetJsonDecimal(receiptData, "amount") ?? 0;
+
+ // Store original values
+ originalCustomerId = customerId;
+ originalReceiptDate = receiptDate;
+ originalAccountToDebitId = accountToDebitId;
+ originalAccountToCreditId = accountToCreditId;
+ originalAmount = amount;
+
+ // If credit account is empty but customer is set, auto-populate it
+ if (accountToCreditId == 0 && customerId > 0 && customerAdvanceAccounts.TryGetValue(customerId, out var
+ advanceAccountId))
+ {
+ accountToCreditId = advanceAccountId;
+ }
+
+ if (receiptData.TryGetProperty("receiptDate", out var dateElem))
+ {
+ if (DateTime.TryParse(dateElem.GetString(), out var date))
+ receiptDate = date;
+ }
+ }
+ }
+ else
+ {
+ errorMessage = $"Failed to load receipt. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading receipt: {ex.Message}";
+ }
+ }
+
+ private int? GetJsonInt(JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.ValueKind == JsonValueKind.Number && prop.TryGetInt32(out var val))
+ return val;
+ }
+ return null;
+ }
+
+ private decimal? GetJsonDecimal(JsonElement elem, string propName)
+ {
+ if (elem.TryGetProperty(propName, out var prop))
+ {
+ if (prop.ValueKind == JsonValueKind.Number && prop.TryGetDecimal(out var val))
+ return val;
+ }
+ return null;
+ }
+
+ private string GetJsonPropertyString(JsonElement element, string propertyName)
+ {
+ if (element.TryGetProperty(propertyName, out var property))
+ {
+ if (property.ValueKind == JsonValueKind.String)
+ {
+ return property.GetString() ?? "";
+ }
+ else if (property.ValueKind == JsonValueKind.Number)
+ {
+ return property.GetInt32().ToString();
+ }
+ }
+ return "";
+ }
+
+ private void EnableEdit()
+ {
+ // Store original values
+ originalCustomerId = customerId;
+ originalReceiptDate = receiptDate;
+ originalAccountToDebitId = accountToDebitId;
+ originalAccountToCreditId = accountToCreditId;
+ originalAmount = amount;
+ isEditing = true;
+ }
+
+ private void CancelEdit()
+ {
+ // Restore original values
+ customerId = originalCustomerId;
+ receiptDate = originalReceiptDate;
+ accountToDebitId = originalAccountToDebitId;
+ accountToCreditId = originalAccountToCreditId;
+ amount = originalAmount;
+ isEditing = false;
+ errorMessage = "";
+ }
+
+ private async Task HandleSubmit()
+ {
+ try
+ {
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var payload = new
+ {
+ Id = receiptId,
+ CustomerId = customerId,
+ ReceiptDate = receiptDate,
+ Amount = amount,
+ AccountToDebitId = accountToDebitId,
+ AccountToCreditId = accountToCreditId
+ };
+
+ var client = ClientFactory.CreateClient();
+ var json = JsonSerializer.Serialize(payload);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ // Use UpdateReceipt endpoint when editing
+ var response = await client.PostAsync($"{apiurl}sales/updatereceipt", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ isEditing = false;
+ // Reload the receipt data
+ await LoadReceipt();
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ errorMessage = $"Failed to save receipt. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error saving receipt: {ex.Message}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Sales/SalesReceipts.razor b/src/AccountGoWeb/Components/Pages/Sales/SalesReceipts.razor
new file mode 100644
index 000000000..f3623c4e3
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Sales/SalesReceipts.razor
@@ -0,0 +1,316 @@
+@using Dto.Sales
+@using System.Text.Json
+@using System.Text.Json.Serialization
+
+
+
+
+
+
+ @* SALES RECEIPTS LIST *@
+
+ @if (loading)
+ {
+
Loading...
+ }
+ else if (errorMessage != null)
+ {
+
+ Error: @errorMessage
+
+ }
+ else if (salesReceipts.Count == 0)
+ {
+
+ No sales receipts found.
+
+ }
+ else
+ {
+
+
+
+
+ Receipt ID
+ Receipt No
+ Customer Name
+ Receipt Date
+ Amount
+ Left to Allocate
+
+
+
+ @if (salesReceipts.Count > 0)
+ {
+ @for (int i = 0; i < salesReceipts.Count; i++)
+ {
+ int index = i;
+ OnRowSelected(index))">
+ @GetValue(salesReceipts[index], "id")
+ @GetValue(salesReceipts[index], "receiptNo")
+ @GetValue(salesReceipts[index], "customerName")
+ @FormatDate(GetValue(salesReceipts[index], "receiptDate"))
+ @FormatAmount(GetValue(salesReceipts[index], "amount"))
+ @FormatAmount(GetValue(salesReceipts[index], "remainingAmountToAllocate"))
+
+ }
+ }
+
+
+
+ }
+
+
+@code {
+ [Inject]
+ private IHttpClientFactory ClientFactory { get; set; } = null!;
+
+ [Inject]
+ private NavigationManager Navigation { get; set; } = null!;
+
+ private List salesReceipts = new();
+ private bool loading = true;
+ private string? errorMessage = null;
+ private bool shouldRefresh = false;
+ private string? selectedReceiptId = null;
+ private int selectedRowIndex = -1;
+ private bool isAllocateDisabled = false;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadSalesReceipts();
+ Navigation.LocationChanged += OnLocationChanged;
+ }
+
+ private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
+ {
+ // Refresh data when navigating back to this page
+ if (e.Location.Contains("/sales/salesreceipts") || e.Location.Contains("/sales/SalesReceipts"))
+ {
+ shouldRefresh = true;
+ InvokeAsync(StateHasChanged);
+ }
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (shouldRefresh && !firstRender)
+ {
+ shouldRefresh = false;
+ await LoadSalesReceipts();
+ }
+ }
+
+ public void Dispose()
+ {
+ Navigation.LocationChanged -= OnLocationChanged;
+ }
+
+ private async Task LoadSalesReceipts()
+ {
+ try
+ {
+ loading = true;
+ string apiurl = System.Environment.GetEnvironmentVariable("APIURL") ?? "http://localhost:8001/api/";
+
+ var client = ClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(60);
+
+ var response = await client.GetAsync($"{apiurl}sales/salesreceipts");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = await response.Content.ReadAsStringAsync();
+ var options = new System.Text.Json.JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ var data = System.Text.Json.JsonSerializer.Deserialize>(responseString, options);
+ salesReceipts = data?.Cast().ToList() ?? new();
+ }
+ else
+ {
+ errorMessage = $"Failed to load sales receipts. Status: {response.StatusCode}";
+ }
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Error loading sales receipts: {ex.Message}";
+ }
+ finally
+ {
+ loading = false;
+ }
+ }
+
+ private string GetValue(dynamic obj, string propertyName)
+ {
+ try
+ {
+ if (obj is System.Text.Json.JsonElement je)
+ {
+ if (je.TryGetProperty(propertyName, out var prop))
+ {
+ if ((propertyName == "amount" || propertyName == "remainingAmountToAllocate") && prop.ValueKind ==
+ System.Text.Json.JsonValueKind.Number)
+ {
+ return prop.GetDecimal().ToString("F2");
+ }
+ // Handle numeric ID
+ if (propertyName == "id" && prop.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ return prop.GetInt32().ToString();
+ }
+ return prop.GetString() ?? prop.ToString();
+ }
+ }
+ return "N/A";
+ }
+ catch
+ {
+ return "N/A";
+ }
+ }
+
+ private string FormatAmount(string value)
+ {
+ if (decimal.TryParse(value, out var amount))
+ {
+ return amount.ToString("F2");
+ }
+ return value;
+ }
+
+ private string FormatDate(string value)
+ {
+ if (DateTime.TryParse(value, out var date))
+ {
+ return date.ToString("yyyy-MM-dd");
+ }
+ return value;
+ }
+
+ private void OnRowSelected(int index)
+ {
+ // Toggle selection: deselect if clicking the same row
+ if (selectedRowIndex == index)
+ {
+ selectedRowIndex = -1;
+ selectedReceiptId = null;
+ isAllocateDisabled = false;
+ return;
+ }
+
+ selectedRowIndex = index;
+
+ try
+ {
+ if (index >= 0 && index < salesReceipts.Count)
+ {
+ var receipt = salesReceipts[index];
+ if (receipt is System.Text.Json.JsonElement je)
+ {
+ string? receiptId = null;
+
+ if (je.TryGetProperty("id", out var idProp))
+ {
+ if (idProp.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ receiptId = idProp.GetString();
+ }
+ else if (idProp.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ receiptId = idProp.GetInt32().ToString();
+ }
+ }
+ else if (je.TryGetProperty("Id", out var idProp2))
+ {
+ if (idProp2.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ receiptId = idProp2.GetString();
+ }
+ else if (idProp2.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ receiptId = idProp2.GetInt32().ToString();
+ }
+ }
+
+ if (!string.IsNullOrEmpty(receiptId))
+ {
+ selectedReceiptId = receiptId;
+
+ // Check if there's remaining amount to allocate
+ isAllocateDisabled = true;
+ if (je.TryGetProperty("remainingAmountToAllocate", out var amountProp))
+ {
+ try
+ {
+ if (amountProp.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ var remaining = amountProp.GetDecimal();
+ isAllocateDisabled = remaining <= 0;
+ }
+ }
+ catch
+ {
+ // Amount might not be a number, skip
+ }
+ }
+
+ StateHasChanged();
+ }
+ }
+ }
+ }
+ catch
+ {
+ selectedReceiptId = null;
+ }
+ }
+
+ private string GetViewReceiptLink()
+ {
+ return selectedReceiptId != null ? $"/Sales/SalesReceipt?id={selectedReceiptId}" : "javascript:void(0)";
+ }
+
+ private string GetAllocateLink()
+ {
+ return selectedReceiptId != null ? $"/Sales/Allocate/{selectedReceiptId}" : "javascript:void(0)";
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Students.razor b/src/AccountGoWeb/Components/Pages/Students.razor
new file mode 100644
index 000000000..443d791b0
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Students.razor
@@ -0,0 +1,23 @@
+@page "/students"
+@rendermode InteractiveServer
+Students
+Students
+
+
+
+
+
+ @context.FirstName @context.LastName
+
+
+
+
+
+
+
+@code {
+ IQueryable students = Student.GetStudents();
+ PaginationState pagination = new PaginationState { ItemsPerPage = 10 };
+ GridSort sortByName = GridSort
+ .ByAscending(_ => _.FirstName).ThenAscending(_ => _.LastName);
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Pages/Tax/TaxManagement.razor b/src/AccountGoWeb/Components/Pages/Tax/TaxManagement.razor
new file mode 100644
index 000000000..dd14b0a1b
--- /dev/null
+++ b/src/AccountGoWeb/Components/Pages/Tax/TaxManagement.razor
@@ -0,0 +1,428 @@
+@rendermode InteractiveServer
+@inject HttpClient Http
+@inject NavigationManager Navigation
+@inject IConfiguration Configuration
+@inject IJSRuntime JSRuntime
+
+Tax Management
+
+
+
+
+
+ @if (activeTab == "taxes")
+ {
+
+
+
+
+
+
+ Code
+ Name
+ Rate (%)
+ Action
+
+
+
+ @if (taxes != null && taxes.Any())
+ {
+ @foreach (var tax in taxes)
+ {
+
+ @tax.TaxCode
+ @tax.TaxName
+ @tax.Rate
+
+ EditTax(tax)">Edit
+ ConfirmDeleteTax(tax.Id)">Delete
+
+
+ }
+ }
+ else if (isLoading)
+ {
+
+ Loading...
+
+ }
+ else
+ {
+
+ No taxes found.
+
+ }
+
+
+
+
+ }
+ else if (activeTab == "tax-groups")
+ {
+
+
+
+ @if (selectedTaxGroup != null)
+ {
+
+
Tax(es) included: @GetTaxGroupTaxCount(selectedTaxGroup)
+
+
+
+
+ Code
+ Name
+ Rate (%)
+
+
+
+ @foreach (var groupTax in GetTaxGroupTaxes(selectedTaxGroup))
+ {
+
+ @groupTax.TaxCode
+ @groupTax.TaxName
+ @groupTax.Rate
+
+ }
+
+
+
+
+ }
+
+ }
+ else if (activeTab == "item-tax-groups")
+ {
+
+
+
+ @if (selectedItemTaxGroup != null)
+ {
+
+
Tax(es) included: @GetItemTaxGroupTaxCount(selectedItemTaxGroup)
+
+
+
+
+ Code
+ Name
+ Rate (%)
+
+
+
+ @foreach (var groupTax in GetItemTaxGroupTaxes(selectedItemTaxGroup))
+ {
+
+ @groupTax.TaxCode
+ @groupTax.TaxName
+ @groupTax.Rate
+
+ }
+
+
+
+
+ }
+
+ }
+
+
+
+@* Delete Confirmation Modals would use JavaScript interop or a confirmation component *@
+
+@code {
+ private string activeTab = "taxes";
+ private bool isLoading = true;
+
+ private List? taxes;
+ private List? taxGroups;
+ private List? itemTaxGroups;
+
+ private Dto.TaxSystem.TaxGroup? selectedTaxGroup;
+ private Dto.TaxSystem.ItemTaxGroup? selectedItemTaxGroup;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadTaxData();
+ }
+
+ private async Task LoadTaxData()
+ {
+ isLoading = true;
+ try
+ {
+ var baseUri = Configuration["ApiUrl"];
+ var response = await Http.GetAsync($"{baseUri}tax/taxes");
+
+ if (response.IsSuccessStatusCode)
+ {
+ var content = await response.Content.ReadAsStringAsync();
+ var taxSystemDto = System.Text.Json.JsonSerializer.Deserialize(
+ content,
+ new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
+ );
+
+ taxes = taxSystemDto?.Taxes?.ToList();
+ taxGroups = taxSystemDto?.TaxGroups?.ToList();
+ itemTaxGroups = taxSystemDto?.ItemTaxGroups?.ToList();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error loading tax data: {ex.Message}");
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SetActiveTab(string tab)
+ {
+ activeTab = tab;
+ selectedTaxGroup = null;
+ selectedItemTaxGroup = null;
+ }
+
+ private void EditTax(Dto.TaxSystem.Tax tax)
+ {
+ Navigation.NavigateTo($"/Tax/EditTax?id={tax.Id}", forceLoad: true);
+ }
+
+ private async Task ConfirmDeleteTax(int taxId)
+ {
+ bool confirmed = await JSRuntime.InvokeAsync("confirmDialog.show", "Are you sure you want to delete this tax?");
+ if (confirmed)
+ {
+ try
+ {
+ var baseUri = Configuration["ApiUrl"];
+ var response = await Http.DeleteAsync($"{baseUri}tax/deletetax?id={taxId}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ await LoadTaxData(); // Reload the data
+ }
+ else
+ {
+ Console.WriteLine($"Error deleting tax: {response.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting tax: {ex.Message}");
+ }
+ }
+ }
+
+ private void SelectTaxGroup(Dto.TaxSystem.TaxGroup group)
+ {
+ selectedTaxGroup = group;
+ }
+
+ private async Task ConfirmDeleteTaxGroup(int groupId)
+ {
+ bool confirmed = await JSRuntime.InvokeAsync("confirmDialog.show", "Are you sure you want to delete this tax group?");
+ if (confirmed)
+ {
+ try
+ {
+ var baseUri = Configuration["ApiUrl"];
+ var response = await Http.DeleteAsync($"{baseUri}tax/deletetaxgroup?id={groupId}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ await LoadTaxData(); // Reload the data
+ selectedTaxGroup = null;
+ }
+ else
+ {
+ Console.WriteLine($"Error deleting tax group: {response.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting tax group: {ex.Message}");
+ }
+ }
+ }
+
+ private void SelectItemTaxGroup(Dto.TaxSystem.ItemTaxGroup group)
+ {
+ selectedItemTaxGroup = group;
+ }
+
+ private async Task ConfirmDeleteItemTaxGroup(int groupId)
+ {
+ bool confirmed = await JSRuntime.InvokeAsync("confirmDialog.show", "Are you sure you want to delete this item tax group?");
+ if (confirmed)
+ {
+ try
+ {
+ var baseUri = Configuration["ApiUrl"];
+ var response = await Http.DeleteAsync($"{baseUri}tax/deleteitemtaxgroup?id={groupId}");
+
+ if (response.IsSuccessStatusCode)
+ {
+ await LoadTaxData(); // Reload the data
+ selectedItemTaxGroup = null;
+ }
+ else
+ {
+ Console.WriteLine($"Error deleting item tax group: {response.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting item tax group: {ex.Message}");
+ }
+ }
+ }
+
+ private int GetTaxGroupTaxCount(Dto.TaxSystem.TaxGroup group)
+ {
+ return group.Taxes?.Count() ?? 0;
+ }
+
+ private IEnumerable GetTaxGroupTaxes(Dto.TaxSystem.TaxGroup group)
+ {
+ if (group.Taxes == null || taxes == null) return Enumerable.Empty();
+
+ return group.Taxes
+ .Select(gt => taxes.FirstOrDefault(t => t.Id == gt.TaxId))
+ .Where(t => t != null)
+ .Cast();
+ }
+
+ private int GetItemTaxGroupTaxCount(Dto.TaxSystem.ItemTaxGroup group)
+ {
+ return group.Taxes?.Count() ?? 0;
+ }
+
+ private IEnumerable GetItemTaxGroupTaxes(Dto.TaxSystem.ItemTaxGroup group)
+ {
+ if (group.Taxes == null || taxes == null) return Enumerable.Empty();
+
+ return group.Taxes
+ .Select(gt => taxes.FirstOrDefault(t => t.Id == gt.TaxId))
+ .Where(t => t != null)
+ .Cast();
+ }
+}
+
+
\ No newline at end of file
diff --git a/src/AccountGoWeb/Components/Routes.razor b/src/AccountGoWeb/Components/Routes.razor
new file mode 100644
index 000000000..da8815572
--- /dev/null
+++ b/src/AccountGoWeb/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/AccountGoWeb/Components/_Imports.razor b/src/AccountGoWeb/Components/_Imports.razor
new file mode 100644
index 000000000..52e49f513
--- /dev/null
+++ b/src/AccountGoWeb/Components/_Imports.razor
@@ -0,0 +1,19 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using Microsoft.AspNetCore.Components.QuickGrid
+@using AccountGoWeb
+@using AccountGoWeb.Components
+@using AccountGoWeb.Models
+@using AccountGoWeb.Models.Account
+@using AccountGoWeb.Models.Bogus
+@using AccountGoWeb.Models.Financial
+@using AccountGoWeb.Models.Purchasing
+@using AccountGoWeb.Models.Sales
+@using AccountGoWeb.Models.TaxSystem
+@using Dto.Donations
diff --git a/src/AccountGoWeb/Controllers/AccountController.cs b/src/AccountGoWeb/Controllers/AccountController.cs
index 0eba93c37..18fb676a6 100644
--- a/src/AccountGoWeb/Controllers/AccountController.cs
+++ b/src/AccountGoWeb/Controllers/AccountController.cs
@@ -3,25 +3,20 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
using System.Security.Claims;
-using System.Threading.Tasks;
namespace AccountGoWeb.Controllers
{
- public class AccountController : BaseController
+ public class AccountController : GoodController
{
public AccountController(IConfiguration config)
{
- _baseConfig = config;
+ _configuration = config;
}
[HttpGet]
[AllowAnonymous]
- public IActionResult SignIn(string returnUrl = null)
+ public IActionResult SignIn(string? returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View(new LoginViewModel() { Email = "admin@accountgo.ph", Password = "P@ssword1" });
@@ -29,7 +24,7 @@ public IActionResult SignIn(string returnUrl = null)
[HttpPost]
[AllowAnonymous]
- public async Task SignIn(LoginViewModel model, string returnUrl = null)
+ public async Task SignIn(LoginViewModel model, string? returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
@@ -47,8 +42,8 @@ public async Task SignIn(LoginViewModel model, string returnUrl =
var claims = new List();
claims.Add(new Claim(ClaimTypes.IsPersistent, model.RememberMe.ToString()));
- claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Email));
- claims.Add(new Claim(ClaimTypes.Email, user.Email));
+ claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Email!));
+ claims.Add(new Claim(ClaimTypes.Email, user.Email!));
string firstName = user.FirstName != null ? user.FirstName : "";
string lastName = user.LastName != null ? user.LastName : "";
@@ -58,7 +53,7 @@ public async Task SignIn(LoginViewModel model, string returnUrl =
claims.Add(new Claim(ClaimTypes.Name, firstName + " " + lastName));
foreach(var role in user.Roles)
- claims.Add(new Claim(ClaimTypes.Role, role.Name));
+ claims.Add(new Claim(ClaimTypes.Role, role.Name!));
claims.Add(new Claim(ClaimTypes.UserData, Newtonsoft.Json.JsonConvert.SerializeObject(user)));
@@ -70,7 +65,7 @@ public async Task SignIn(LoginViewModel model, string returnUrl =
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
- return RedirectToLocal(returnUrl);
+ return RedirectToLocal(returnUrl!);
}
else
{
@@ -83,7 +78,7 @@ public async Task SignIn(LoginViewModel model, string returnUrl =
return View(model);
}
- public async Task SignOut()
+ public async Task Logout()
{
await HttpContext.SignOutAsync();
@@ -92,7 +87,7 @@ public async Task SignOut()
public IActionResult SignedOut()
{
- if (HttpContext.User.Identity.IsAuthenticated)
+ if (HttpContext.User.Identity!.IsAuthenticated)
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
@@ -106,7 +101,7 @@ public IActionResult Unauthorize()
[HttpGet]
[AllowAnonymous]
- public IActionResult Register(string returnUrl = null)
+ public IActionResult Register(string? returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
@@ -114,7 +109,7 @@ public IActionResult Register(string returnUrl = null)
[HttpPost]
[AllowAnonymous]
- public IActionResult Register(RegisterViewModel model, string returnUrl = null)
+ public IActionResult Register(RegisterViewModel model, string? returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
try
@@ -127,9 +122,9 @@ public IActionResult Register(RegisterViewModel model, string returnUrl = null)
HttpResponseMessage responseAddNewUser = Post("account/addnewuser", content);
Newtonsoft.Json.Linq.JObject resultAddNewUser = Newtonsoft.Json.Linq.JObject.Parse(responseAddNewUser.Content.ReadAsStringAsync().Result);
- HttpResponseMessage responseInitialized = null;
- Newtonsoft.Json.Linq.JObject resultInitialized = null;
- if ((bool)resultAddNewUser["succeeded"])
+ HttpResponseMessage? responseInitialized = null;
+ Newtonsoft.Json.Linq.JObject? resultInitialized = null;
+ if ((bool)resultAddNewUser["succeeded"]!)
{
responseInitialized = Get("administration/initializedcompany");
resultInitialized = Newtonsoft.Json.Linq.JObject.Parse((responseInitialized.Content.ReadAsStringAsync().Result));
@@ -137,7 +132,7 @@ public IActionResult Register(RegisterViewModel model, string returnUrl = null)
}
else
{
- ModelState.AddModelError(string.Empty, resultAddNewUser["errors"][0]["description"].ToString());
+ ModelState.AddModelError(string.Empty, resultAddNewUser["errors"]![0]!["description"]!.ToString());
return View(model);
}
}
diff --git a/src/AccountGoWeb/Controllers/AdministrationController.cs b/src/AccountGoWeb/Controllers/AdministrationController.cs
index a05f3f301..148b81334 100644
--- a/src/AccountGoWeb/Controllers/AdministrationController.cs
+++ b/src/AccountGoWeb/Controllers/AdministrationController.cs
@@ -1,14 +1,11 @@
using Dto.Administration;
using Dto.Security;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using System;
-using System.Net.Http;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
- public class AdministrationController : BaseController
+ //[Microsoft.AspNetCore.Authorization.Authorize]
+ public class AdministrationController : BaseController
{
public AdministrationController(IConfiguration config)
{
@@ -125,13 +122,13 @@ public async System.Threading.Tasks.Task AuditLogs()
HttpResponseMessage responseAddNewUser = Post("account/addnewuser", content);
Newtonsoft.Json.Linq.JObject resultAddNewUser = Newtonsoft.Json.Linq.JObject.Parse(responseAddNewUser.Content.ReadAsStringAsync().Result);
- if ((bool)resultAddNewUser["succeeded"])
+ if ((bool)resultAddNewUser["succeeded"]!)
{
return RedirectToAction(nameof(AdministrationController.Users), "Administration");
}
else
{
- ModelState.AddModelError(string.Empty, resultAddNewUser["errors"][0]["description"].ToString());
+ ModelState.AddModelError(string.Empty, resultAddNewUser["errors"]![0]!["description"]!.ToString());
return View(model);
}
}
diff --git a/src/AccountGoWeb/Controllers/AuditController.cs b/src/AccountGoWeb/Controllers/AuditController.cs
new file mode 100644
index 000000000..5802766cd
--- /dev/null
+++ b/src/AccountGoWeb/Controllers/AuditController.cs
@@ -0,0 +1,146 @@
+using System.Text;
+using Dto.Auditing;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+
+
+namespace AccountGoWeb.Controllers
+{
+ /*
+ This controller provides web views for managing auditable entities and attributes.
+ NOTE: Manages both Auditable Entities and Auditable Attributes.
+ */
+ public class AuditController : BaseController
+ {
+ private readonly ILogger _logger;
+
+ public AuditController(IConfiguration config, ILogger logger)
+ {
+ _baseConfig = config;
+ _logger = logger;
+ }
+
+ // #####Auditable Entities#####
+
+ // Returns a view listing all auditable entities
+ public async Task GetAuditableEntities()
+ {
+ ViewBag.PageContentHeader = "Auditable Entities";
+
+ var entities = await GetAsync>("audit/entities");
+ return View(entities);
+ }
+
+ // Returns a view for a specific auditable entity by ID
+ public async Task GetEntity(int? id = null)
+ {
+ AuditableEntity model;
+
+ if(id == null)
+ {
+ // If no ID is provided, create a new AuditableEntity model
+ model = new AuditableEntity()
+ {
+ EnableAudit = true
+ };
+ }
+ else
+ {
+ model = await GetAsync($"audit/entity?id={id}");
+ }
+
+ ViewBag.PageContentHeader = "Auditable Entity";
+ return View(model);
+ }
+
+ // Saves an auditable entity (new or existing). This do updates via POST.
+ [HttpPost]
+ public async Task SaveEntity(AuditableEntity model)
+ {
+ if (!ModelState.IsValid)
+ {
+ return View("GetEntity", model);
+ }
+
+ var json = JsonConvert.SerializeObject(model);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ await PostAsync("audit/entity", content);
+
+ return RedirectToAction(nameof(GetAuditableEntities));
+ }
+
+ public async Task DeleteEntity(int id)
+ {
+ await DeleteAsync($"audit/entity/{id}");
+ return RedirectToAction(nameof(GetAuditableEntities));
+ }
+
+
+
+ // #####Auditable Attributes#####
+
+ // Returns a view listing all auditable attributes for a specific entity
+ public async Task GetAuditableAttributes(int entityId)
+ {
+ ViewBag.PageContentHeader = "Auditable Attributes";
+ ViewBag.EntityId = entityId;
+
+ var attributes = await GetAsync>($"audit/attributes?entityId={entityId}");
+ return View(attributes);
+ }
+
+ // Returns a view for a specific auditable attribute by ID or a new one if no ID is provided.
+ public async Task GetAttribute(int? id, int entityId)
+ {
+ AuditableAttribute model;
+
+ if (id == null)
+ {
+ // If no ID is provided, create new AuditableAttribute
+ model = new AuditableAttribute()
+ {
+ AuditableEntityId = entityId,
+ EnableAudit = true
+ };
+ }
+ else
+ {
+ model = await GetAsync($"audit/attribute?id={id}");
+ }
+
+ ViewBag.PageContentHeader = "Auditable Attribute";
+ ViewBag.EntityId = entityId;
+
+ return View(model);
+ }
+
+ // Saves an auditable attribute (new or existing). This do updates via POST.
+ [HttpPost]
+ public async Task SaveAttribute(AuditableAttribute model, int entityId)
+ {
+ if (!ModelState.IsValid)
+ {
+ return View("GetAttribute", model);
+ }
+
+ // This line make sure the attribute is linked to the correct entity
+ model.AuditableEntityId = entityId;
+
+ var json = JsonConvert.SerializeObject(model);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ await PostAsync("audit/attribute", content);
+
+ return RedirectToAction(nameof(GetAuditableAttributes), new { entityId });
+ }
+
+ // Deletes an auditable attribute by ID
+ public async Task DeleteAttribute(int id, int entityId)
+ {
+ await DeleteAsync($"audit/attribute/{id}");
+ return RedirectToAction(nameof(GetAuditableAttributes), new { entityId });
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AccountGoWeb/Controllers/BaseController.cs b/src/AccountGoWeb/Controllers/BaseController.cs
index aafb66c5b..31340cc24 100644
--- a/src/AccountGoWeb/Controllers/BaseController.cs
+++ b/src/AccountGoWeb/Controllers/BaseController.cs
@@ -1,20 +1,18 @@
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using System.Net.Http;
namespace AccountGoWeb.Controllers
{
public class BaseController : Controller
{
- protected IConfiguration _baseConfig;
+ protected IConfiguration? _baseConfig;
protected async System.Threading.Tasks.Task GetAsync(string uri)
{
string responseJson = string.Empty;
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = await client.GetAsync(baseUri + uri);
if (response.IsSuccessStatusCode)
@@ -22,7 +20,7 @@ protected async System.Threading.Tasks.Task GetAsync(string uri)
responseJson = await response.Content.ReadAsStringAsync();
}
}
- return Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson);
+ return Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson)!;
}
protected HttpResponseMessage Get(string uri)
@@ -30,21 +28,44 @@ protected HttpResponseMessage Get(string uri)
string responseJson = string.Empty;
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = client.GetAsync(baseUri + uri);
return response.Result;
}
}
+ /*
+ This method performs an HTTP DELETE request to the specified URI.
+ Used in AuditController to delete auditable entities and attributes.
+ */
+ protected async System.Threading.Tasks.Task DeleteAsync(string uri)
+ {
+ using (var client = new HttpClient())
+ {
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ client.DefaultRequestHeaders.Add("UserName", GetCurrentUserName());
+
+ var response = await client.DeleteAsync(baseUri + uri);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var message = await response.Content.ReadAsStringAsync();
+ throw new System.Exception($"DELETE {uri} failed: {message}");
+ }
+ }
+ }
+
protected async System.Threading.Tasks.Task PostAsync(string uri, StringContent data)
{
string responseJson = string.Empty;
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Add("UserName", GetCurrentUserName());
@@ -55,7 +76,7 @@ protected async System.Threading.Tasks.Task PostAsync(string uri, String
}
}
- return Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson);
+ return Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson)!;
}
protected HttpResponseMessage Post(string uri, StringContent data)
@@ -63,8 +84,8 @@ protected HttpResponseMessage Post(string uri, StringContent data)
string responseJson = string.Empty;
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("UserName", GetCurrentUserName());
@@ -76,7 +97,7 @@ protected HttpResponseMessage Post(string uri, StringContent data)
protected bool HasPermission(string permission)
{
- if (HttpContext.User.Identity.IsAuthenticated)
+ if (HttpContext.User.Identity!.IsAuthenticated)
{
System.Collections.Generic.IList permissions = new System.Collections.Generic.List();
@@ -87,11 +108,11 @@ protected bool HasPermission(string permission)
if (current.Type == System.Security.Claims.ClaimTypes.UserData)
{
Newtonsoft.Json.Linq.JObject userData = Newtonsoft.Json.Linq.JObject.Parse(current.Value);
- foreach(var r in userData["Roles"])
+ foreach(var r in userData["Roles"]!)
{
- foreach(var p in r["Permissions"])
+ foreach(var p in r["Permissions"]!)
{
- permissions.Add(p["Name"].ToString());
+ permissions.Add(p["Name"]!.ToString());
}
}
}
@@ -105,7 +126,7 @@ protected bool HasPermission(string permission)
protected string GetCurrentUserName()
{
- if (HttpContext.User.Identity.IsAuthenticated)
+ if (HttpContext.User.Identity!.IsAuthenticated)
{
var claimsEnumerator = HttpContext.User.Claims.GetEnumerator();
while (claimsEnumerator.MoveNext())
diff --git a/src/AccountGoWeb/Controllers/ContactController.cs b/src/AccountGoWeb/Controllers/ContactController.cs
index fd3e1a60a..f22788253 100644
--- a/src/AccountGoWeb/Controllers/ContactController.cs
+++ b/src/AccountGoWeb/Controllers/ContactController.cs
@@ -1,11 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
+using Dto.Common;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using System.Net.Http;
-using Dto.Common;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
namespace AccountGoWeb.Controllers
@@ -35,8 +29,8 @@ public async System.Threading.Tasks.Task Contacts(int partyId = 0
//return View(model: contacts);
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = await client.GetAsync(baseUri + "contact/contacts?partyId=" + partyId + "&partyType=" + partyType);
if (response.IsSuccessStatusCode)
@@ -57,7 +51,7 @@ public async System.Threading.Tasks.Task Contacts(int partyId = 0
///
public IActionResult Contact(int id = 0, int partyId = 0, int partyType = 0)
{
- Contact contact = null;
+ Contact? contact = null;
if (id == 0) // creating new contact
{
diff --git a/src/AccountGoWeb/Controllers/DashboardController.cs b/src/AccountGoWeb/Controllers/DashboardController.cs
index c8791445a..0753e5208 100644
--- a/src/AccountGoWeb/Controllers/DashboardController.cs
+++ b/src/AccountGoWeb/Controllers/DashboardController.cs
@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
+ //[Microsoft.AspNetCore.Authorization.Authorize]
public class DashboardController : BaseController
{
public DashboardController(IConfiguration config)
@@ -19,7 +18,7 @@ public IActionResult Index()
public IActionResult MonthlySales()
{
- ViewBag.ApiMontlySales = _baseConfig["ApiUrl"] + "sales/getmonthlysales";
+ ViewBag.ApiMontlySales = _baseConfig!["ApiUrl"] + "sales/getmonthlysales";
return View();
}
}
diff --git a/src/AccountGoWeb/Controllers/DonationsController.cs b/src/AccountGoWeb/Controllers/DonationsController.cs
new file mode 100644
index 000000000..fc4797ef6
--- /dev/null
+++ b/src/AccountGoWeb/Controllers/DonationsController.cs
@@ -0,0 +1,206 @@
+using AccountGoWeb.Models;
+using Dto.Donations;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+
+namespace AccountGoWeb.Controllers
+{
+ public class DonationsController : GoodController
+ {
+ private readonly ILogger _logger;
+
+ public DonationsController(IConfiguration config, ILogger logger)
+ {
+ _configuration = config;
+ Models.SelectListItemHelper._config = config;
+ _logger = logger;
+ }
+
+ public IActionResult Index()
+ {
+ return RedirectToAction("DonationInvoices");
+ }
+
+ public async System.Threading.Tasks.Task DonationInvoices()
+ {
+ ViewBag.PageContentHeader = "Donation Invoices";
+ using (var client = new HttpClient())
+ {
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ var response = await client.GetAsync(baseUri + "donations/donationinvoices");
+ if (response.IsSuccessStatusCode)
+ {
+ var responseJson = await response.Content.ReadAsStringAsync();
+ return View(model: responseJson);
+ }
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+ }
+ return View();
+ }
+
+ [HttpGet]
+ public IActionResult AddDonationInvoice()
+ {
+ ViewBag.PageContentHeader = "Add Donation Invoice";
+
+ DonationInvoice donationInvoiceModel = new DonationInvoice();
+ donationInvoiceModel.DonationInvoiceLines = new List {
+ new DonationInvoiceLine {
+ Amount = 0,
+ ItemId = 1,
+ Quantity = 1,
+ }
+ };
+ donationInvoiceModel.No = new System.Random().Next(1, 99999).ToString();
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(donationInvoiceModel);
+ }
+
+ [HttpPost]
+ public async System.Threading.Tasks.Task AddDonationInvoice(DonationInvoice Dto, string? addRowBtn)
+ {
+ if (!string.IsNullOrEmpty(addRowBtn))
+ {
+ Dto.DonationInvoiceLines!.Add(new DonationInvoiceLine
+ {
+ Amount = 0,
+ Quantity = 1,
+ ItemId = 1,
+ MeasurementId = 1,
+ });
+
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(Dto);
+ }
+ else if (ModelState.IsValid)
+ {
+ _logger.LogInformation("Posted value received: {Posted}", Dto.Posted);
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(Dto);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ _logger.LogInformation("AddDonationInvoice: " + await content.ReadAsStringAsync());
+ var response = Post("Donations/CreateDonationInvoice", content);
+
+ _logger.LogInformation("AddDonationInvoice response: " + response.ToString());
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("donationinvoices");
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ _logger.LogError("Failed to create donation invoice. Status: {Status}, Error: {Error}", response.StatusCode, errorContent);
+ ModelState.AddModelError("", $"Failed to save donation invoice: {response.StatusCode}");
+ }
+ }
+ else
+ {
+ _logger.LogWarning("ModelState is invalid. Errors: {Errors}",
+ string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)));
+ }
+
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(Dto);
+ }
+
+ public IActionResult DonationInvoice(int id)
+ {
+ ViewBag.PageContentHeader = "Donation Invoice";
+ DonationInvoice? donationInvoiceModel = null;
+
+ if (id == 0)
+ {
+ ViewBag.PageContentHeader = "Add Donation Invoice";
+ return View("AddDonationInvoice");
+ }
+ else
+ {
+ donationInvoiceModel = GetAsync("Donations/DonationInvoice?id=" + id).Result;
+ ViewBag.Id = donationInvoiceModel.Id;
+ ViewBag.DonorName = donationInvoiceModel.DonorName;
+ ViewBag.DonationDate = donationInvoiceModel.DonationDate;
+ ViewBag.DonationInvoiceLines = donationInvoiceModel.DonationInvoiceLines;
+ ViewBag.TotalAmount = donationInvoiceModel.Amount;
+ }
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View("DonationInvoice", donationInvoiceModel);
+ }
+
+ [HttpPost]
+ public async System.Threading.Tasks.Task DonationInvoice(DonationInvoice donationInvoiceModel)
+ {
+ if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(donationInvoiceModel);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+ string ReadAsStringAsync = await content.ReadAsStringAsync();
+ _logger.LogInformation("SaveDonationInvoice: " + ReadAsStringAsync);
+ var response = Post("Donations/UpdateDonationInvoice", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ return RedirectToAction("DonationInvoices");
+ }
+ else
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ _logger.LogError("Failed to update donation invoice. Status: {Status}, Error: {Error}", response.StatusCode, errorContent);
+ ModelState.AddModelError("", $"Failed to update donation invoice: {response.StatusCode}");
+ }
+ }
+ else
+ {
+ _logger.LogWarning("ModelState is invalid. Errors: {Errors}",
+ string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)));
+ }
+
+ ViewBag.Customers = SelectListItemHelper.Customers();
+ ViewBag.Items = SelectListItemHelper.Items();
+ ViewBag.Measurements = SelectListItemHelper.Measurements();
+ ViewBag.TotalAmount = donationInvoiceModel.Amount;
+
+ return View(donationInvoiceModel);
+ }
+
+ public async Task DeleteDonationInvoice(int id)
+ {
+ using (var client = new HttpClient())
+ {
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ var response = await client.DeleteAsync(baseUri + "donations/deletedonationinvoice?id=" + id);
+
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("DonationInvoices");
+ }
+
+ return RedirectToAction("DonationInvoices");
+ }
+
+ public IActionResult DonationInvoicePdf(int id)
+ {
+ var donationInvoice = GetAsync("Donations/DonationInvoice?id=" + id).Result;
+ return View(donationInvoice);
+ }
+ }
+}
diff --git a/src/AccountGoWeb/Controllers/FinancialsController.cs b/src/AccountGoWeb/Controllers/FinancialsController.cs
index bbe60f680..b62c23c63 100644
--- a/src/AccountGoWeb/Controllers/FinancialsController.cs
+++ b/src/AccountGoWeb/Controllers/FinancialsController.cs
@@ -1,196 +1,215 @@
using Microsoft.AspNetCore.Mvc;
-using System.Collections.Generic;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
- public class FinancialsController : BaseController
+ //[Microsoft.AspNetCore.Authorization.Authorize]
+ public class FinancialsController : BaseController
+ {
+ private readonly ILogger _logger;
+
+ public FinancialsController(IConfiguration config, ILogger logger)
{
- public FinancialsController(Microsoft.Extensions.Configuration.IConfiguration config)
- {
- _baseConfig = config;
- }
+ _baseConfig = config;
+ _logger = logger;
+ }
- public IActionResult AddJournalEntry()
- {
- ViewBag.PageContentHeader = "Add Journal Entry";
- return View();
- }
+ public IActionResult AddJournalEntry()
+ {
+ ViewBag.PageContentHeader = "Add Journal Entry";
+ return View();
+ }
- public IActionResult JournalEntry(int id)
- {
- ViewBag.PageContentHeader = "Journal Entry";
- return View();
- }
+ public IActionResult JournalEntry(int id)
+ {
+ ViewBag.PageContentHeader = "Journal Entry";
+ return View(model: id);
+ }
- public async System.Threading.Tasks.Task Accounts()
+ public async Task Accounts()
+ {
+ ViewBag.PageContentHeader = "Chart of Accounts";
+
+ using (var client = new System.Net.Http.HttpClient())
+ {
+ var baseUri = _baseConfig!["ApiUrl"];
+ _logger.LogInformation($"+++++++++++++++ baseUri={baseUri} +++++++++++++++");
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ var response = await client.GetAsync(baseUri + "financials/accounts");
+ if (response.IsSuccessStatusCode)
{
- ViewBag.PageContentHeader = "Accounts";
-
- using (var client = new System.Net.Http.HttpClient())
- {
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
- client.DefaultRequestHeaders.Accept.Clear();
- var response = await client.GetAsync(baseUri + "financials/accounts");
- if (response.IsSuccessStatusCode)
- {
- var responseJson = await response.Content.ReadAsStringAsync();
- return View(model: responseJson);
- }
- }
-
- return View();
+ var responseJson = await response.Content.ReadAsStringAsync();
+ var accountModels = Newtonsoft.Json.JsonConvert.DeserializeObject