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..b1423e260 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,240 @@ 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}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "src\Modules\SampleModule\SampleModule.csproj", "{B296277A-C822-444E-8CFA-4CC4C1C1F737}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GoodBooks.BackendTests", "test\GoodBooks.BackendTests\GoodBooks.BackendTests.csproj", "{C59F300E-4BAC-4329-9A41-8F1D75A7E197}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleNetStandard20", "test\SampleModules\SampleNetStandard20\SampleNetStandard20.csproj", "{B0AB6EA7-7D53-4457-9482-F0613F99E3BB}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EF0BD6F1-00D6-41E5-91AB-8B606D35D448}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EFF13E33-1D79-4221-87D7-4FCC8EA88943}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "src\Modules\SampleModule\SampleModule.csproj", "{ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4}"
+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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationService", "src\MigrationService\MigrationService.csproj", "{DF084D96-707B-47C2-9493-85FA84631ACE}"
+EndProject
+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
+ {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.ActiveCfg = Debug|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 +262,20 @@ 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}
+ {1E610F55-2D74-4856-818B-0D0B47601B75} = {EF0BD6F1-00D6-41E5-91AB-8B606D35D448}
{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}
+ {C59F300E-4BAC-4329-9A41-8F1D75A7E197} = {0EAC5155-A5EA-49C1-8E0C-19DD36D2C21C}
{B0AB6EA7-7D53-4457-9482-F0613F99E3BB} = {0EAC5155-A5EA-49C1-8E0C-19DD36D2C21C}
+ {EFF13E33-1D79-4221-87D7-4FCC8EA88943} = {EF0BD6F1-00D6-41E5-91AB-8B606D35D448}
+ {ABD1EE97-DD84-4C6A-8F3F-28E7D3D898B4} = {EFF13E33-1D79-4221-87D7-4FCC8EA88943}
+ {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..389398d75 100644
--- a/src/AccountGoWeb/AccountGoWeb.csproj
+++ b/src/AccountGoWeb/AccountGoWeb.csproj
@@ -1,46 +1,36 @@
-
-
+
- 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..a99d041ea
--- /dev/null
+++ b/src/AccountGoWeb/Components/App.razor
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/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/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..292501ac9
--- /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/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..8eccd6b2e
--- /dev/null
+++ b/src/AccountGoWeb/Components/_Imports.razor
@@ -0,0 +1,18 @@
+@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
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>(responseJson);
+ return View(accountModels);
}
+ }
+
+ return View();
+ }
+
+ public async Task Account(int? id = null)
+ {
+ Dto.Financial.Account? accountModel = null;
+ if (id == null)
+ {
+ accountModel = new Dto.Financial.Account();
+ }
+ else
+ {
+ accountModel = await GetAsync("financials/account?id=" + id);
+ }
+
+ ViewBag.PageContentHeader = "Account";
+ return View(accountModel);
+ }
- public async System.Threading.Tasks.Task Account(int? id = null)
+ public async System.Threading.Tasks.Task JournalEntries()
+ {
+ ViewBag.PageContentHeader = "Journal Entries";
+
+ 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/journalentries");
+ if (response.IsSuccessStatusCode)
{
- Dto.Financial.Account accountModel = null;
- if (id == null)
- {
- accountModel = new Dto.Financial.Account();
- }
- else
- {
- accountModel = await GetAsync("financials/account?id=" + id);
- }
-
- ViewBag.PageContentHeader = "Account";
- return View(accountModel);
+ var responseJson = await response.Content.ReadAsStringAsync();
+ return View(model: responseJson);
}
+ }
+
+ return View();
+ }
- public async System.Threading.Tasks.Task JournalEntries()
+ public async System.Threading.Tasks.Task GeneralLedger()
+ {
+ ViewBag.PageContentHeader = "General Ledger";
+
+ 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/generalledger");
+ if (response.IsSuccessStatusCode)
{
- ViewBag.PageContentHeader = "Journal Entries";
-
- 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/journalentries");
- if (response.IsSuccessStatusCode)
- {
- var responseJson = await response.Content.ReadAsStringAsync();
- return View(model: responseJson);
- }
- }
-
- return View();
+ var responseJson = await response.Content.ReadAsStringAsync();
+ return View(model: responseJson);
}
+ }
+
+ return View();
+ }
- public async System.Threading.Tasks.Task GeneralLedger()
+ public async System.Threading.Tasks.Task TrialBalance()
+ {
+ ViewBag.PageContentHeader = "Trial Balance";
+
+ 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/trialbalance");
+ if (response.IsSuccessStatusCode)
{
- ViewBag.PageContentHeader = "General Ledger";
-
- 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/generalledger");
- if (response.IsSuccessStatusCode)
- {
- var responseJson = await response.Content.ReadAsStringAsync();
- return View(model: responseJson);
- }
- }
-
- return View();
+ var responseJson = await response.Content.ReadAsStringAsync();
+ var trialBalanceModel = Newtonsoft.Json.JsonConvert.DeserializeObject>(responseJson);
+ return View(trialBalanceModel);
}
+ }
+
+ return View();
+ }
- public async System.Threading.Tasks.Task TrialBalance()
+ public async System.Threading.Tasks.Task BalanceSheet()
+ {
+ ViewBag.PageContentHeader = "Balance Sheet";
+
+ 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/balancesheet");
+ if (response.IsSuccessStatusCode)
{
- ViewBag.PageContentHeader = "Trial Balance";
-
- 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/trialbalance");
- if (response.IsSuccessStatusCode)
- {
- var responseJson = await response.Content.ReadAsStringAsync();
- var trialBalanceModel = Newtonsoft.Json.JsonConvert.DeserializeObject>(responseJson);
- return View(trialBalanceModel);
- }
- }
-
- return View();
+ var responseJson = await response.Content.ReadAsStringAsync();
+ var balanceSheetModel = Newtonsoft.Json.JsonConvert.DeserializeObject>(responseJson);
+ return View(balanceSheetModel);
}
+ }
+ return View();
+ // return View(new List()); // Use this statement to test the view with an empty balance sheet
+
+ //var Dto = _financialService.BalanceSheet().ToList();
+ //var dt = Helpers.CollectionHelper.ConvertTo(Dto);
+ //var incomestatement = _financialService.IncomeStatement();
+ //var netincome = incomestatement.Where(a => a.IsExpense == false).Sum(a => a.Amount) - incomestatement.Where(a => a.IsExpense == true).Sum(a => a.Amount);
+
+ // TODO: Add logic to get the correct account for accumulated profit/loss. Currently, the account code is hard-coded here.
+ // Solution 1: Add two columns in general ledger setting for the account id of accumulated profit and loss.
+ // Solution 2: Add column to Account table to flag if account is net income (profit and loss)
+ //if (netincome < 0)
+ //{
+ // var loss = Dto.Where(a => a.AccountCode == "30500").FirstOrDefault();
+ // loss.Amount = netincome;
+ //}
+ //else
+ //{
+ // var profit = Dto.Where(a => a.AccountCode == "30400").FirstOrDefault();
+ // profit.Amount = netincome;
+ //}
+
+ //return View(Dto);
+ }
+
+ public async Task IncomeStatement()
+ {
+ ViewBag.PageContentHeader = "Income Statement";
+
+ using (var client = new System.Net.Http.HttpClient())
+ {
+ var baseUri = _baseConfig!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
- public async System.Threading.Tasks.Task BalanceSheet()
+ try
{
- ViewBag.PageContentHeader = "Balance Sheet";
-
- 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/balancesheet");
- if (response.IsSuccessStatusCode)
- {
- var responseJson = await response.Content.ReadAsStringAsync();
- var balanceSheetModel = Newtonsoft.Json.JsonConvert.DeserializeObject>(responseJson);
- return View(balanceSheetModel);
- }
- }
-
- return View();
- //var Dto = _financialService.BalanceSheet().ToList();
- //var dt = Helpers.CollectionHelper.ConvertTo(Dto);
- //var incomestatement = _financialService.IncomeStatement();
- //var netincome = incomestatement.Where(a => a.IsExpense == false).Sum(a => a.Amount) - incomestatement.Where(a => a.IsExpense == true).Sum(a => a.Amount);
-
- // TODO: Add logic to get the correct account for accumulated profit/loss. Currently, the account code is hard-coded here.
- // Solution 1: Add two columns in general ledger setting for the account id of accumulated profit and loss.
- // Solution 2: Add column to Account table to flag if account is net income (profit and loss)
- //if (netincome < 0)
- //{
- // var loss = Dto.Where(a => a.AccountCode == "30500").FirstOrDefault();
- // loss.Amount = netincome;
- //}
- //else
- //{
- // var profit = Dto.Where(a => a.AccountCode == "30400").FirstOrDefault();
- // profit.Amount = netincome;
- //}
-
- //return View(Dto);
+ var response = await client.GetAsync(baseUri + "financials/incomestatement");
+ if (response.IsSuccessStatusCode)
+ {
+ var responseJson = await response.Content.ReadAsStringAsync();
+ var incomeStatementModel = Newtonsoft.Json.JsonConvert.DeserializeObject>(responseJson);
+ return View(incomeStatementModel);
+ }
+ else
+ {
+ ViewBag.Error = "Failed to fetch income statement data.";
+ }
}
-
- public async System.Threading.Tasks.Task IncomeStatement()
+ catch (Exception ex)
{
- ViewBag.PageContentHeader = "Income Statement";
-
- 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/incomestatement");
- if (response.IsSuccessStatusCode)
- {
- var responseJson = await response.Content.ReadAsStringAsync();
- var incomeStatementModel = Newtonsoft.Json.JsonConvert.DeserializeObject>(responseJson);
- return View(incomeStatementModel);
- }
- }
-
- return View();
- //var Dto = _financialService.IncomeStatement();
- //return View(Dto);
+ ViewBag.Error = $"Error: {ex.Message}";
}
+ }
- public IActionResult Banks()
- {
- ViewBag.PageContentHeader = "Cash/Banks";
+ return View(new List());
+ }
- var banks = GetAsync>("financials/cashbanks").Result;
- return View(banks);
- }
+ public IActionResult Banks()
+ {
+ ViewBag.PageContentHeader = "Cash/Banks";
+
+ var banks = GetAsync>("financials/cashbanks").Result;
+
+ return View(banks);
}
+
+
+
+ }
}
diff --git a/src/AccountGoWeb/Controllers/GoodController.cs b/src/AccountGoWeb/Controllers/GoodController.cs
new file mode 100644
index 000000000..93b676218
--- /dev/null
+++ b/src/AccountGoWeb/Controllers/GoodController.cs
@@ -0,0 +1,74 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace AccountGoWeb.Controllers
+{
+ public class GoodController : Controller
+ {
+ protected IConfiguration? _configuration;
+
+ protected HttpResponseMessage Get(string uri)
+ {
+ string responseJson = string.Empty;
+ using (var client = new HttpClient())
+ {
+ string? baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ var response = client.GetAsync(baseUri + uri);
+ return response.Result;
+ }
+ }
+
+ protected HttpResponseMessage Post(string uri, StringContent data)
+ {
+ string responseJson = string.Empty;
+ using (var client = new HttpClient())
+ {
+ string? baseUri = _configuration!["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());
+
+ var response = client.PostAsync(baseUri + uri, data);
+ return response.Result;
+ }
+ }
+
+ protected async System.Threading.Tasks.Task GetAsync(string uri)
+ {
+ string responseJson = string.Empty;
+ using (var client = new HttpClient())
+ {
+ string? baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ var response = await client.GetAsync(baseUri + uri);
+ if (response.IsSuccessStatusCode)
+ {
+ responseJson = await response.Content.ReadAsStringAsync();
+ }
+ }
+ return Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson)!;
+ }
+
+ protected async System.Threading.Tasks.Task PostAsync(string uri, StringContent data)
+ {
+ string responseJson = string.Empty;
+ using (var client = new HttpClient())
+ {
+ string? baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+ //client.DefaultRequestHeaders.Add("UserName", GetCurrentUserName());
+
+ var response = await client.PostAsync(baseUri + uri, data);
+ if (response.IsSuccessStatusCode)
+ {
+ responseJson = await response.Content.ReadAsStringAsync();
+ }
+ }
+ return Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson)!;
+ }
+ }
+}
diff --git a/src/AccountGoWeb/Controllers/HomeController.cs b/src/AccountGoWeb/Controllers/HomeController.cs
index d7b5a2def..ee4b0b00a 100644
--- a/src/AccountGoWeb/Controllers/HomeController.cs
+++ b/src/AccountGoWeb/Controllers/HomeController.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 HomeController : BaseController
{
public HomeController(IConfiguration config)
@@ -14,7 +13,7 @@ public HomeController(IConfiguration config)
public IActionResult Index()
{
ViewBag.PageContentHeader = "Dashboard";
- ViewBag.ApiMontlySales = _baseConfig["ApiUrl"] + "sales/getmonthlysales";
+ ViewBag.ApiMontlySales = _baseConfig!["ApiUrl"] + "sales/getmonthlysales";
return View();
}
}
diff --git a/src/AccountGoWeb/Controllers/InventoryController.cs b/src/AccountGoWeb/Controllers/InventoryController.cs
index e15c5645b..e357ecce3 100644
--- a/src/AccountGoWeb/Controllers/InventoryController.cs
+++ b/src/AccountGoWeb/Controllers/InventoryController.cs
@@ -1,26 +1,28 @@
using Dto.Inventory;
using Microsoft.AspNetCore.Mvc;
-using System.Net.Http;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
+ // [Microsoft.AspNetCore.Authorization.Authorize]
public class InventoryController : BaseController
{
- public InventoryController(Microsoft.Extensions.Configuration.IConfiguration config)
+ private readonly ILogger _logger;
+ public InventoryController(Microsoft.Extensions.Configuration.IConfiguration config,
+ ILogger logger)
{
_baseConfig = config;
Models.SelectListItemHelper._config = config;
+ _logger = logger;
}
- public async System.Threading.Tasks.Task Items()
+ public async Task Index()
{
ViewBag.PageContentHeader = "Items";
using (var client = new System.Net.Http.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 + "inventory/items");
if (response.IsSuccessStatusCode)
@@ -33,14 +35,14 @@ public async System.Threading.Tasks.Task Items()
return View();
}
- public async System.Threading.Tasks.Task ICJ()
+ public async Task ICJ()
{
ViewBag.PageContentHeader = "Inventory Control Journal";
using (var client = new System.Net.Http.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 + "inventory/icj");
if (response.IsSuccessStatusCode)
@@ -53,9 +55,10 @@ public async System.Threading.Tasks.Task ICJ()
return View();
}
- public IActionResult Item(int id = -1)
+ public IActionResult Item(int id)
{
- Item itemModel = null;
+ _logger.LogInformation("GetItem: " + id);
+ Item? itemModel = null;
if (id == -1)
{
ViewBag.PageContentHeader = "Item Customer";
@@ -76,6 +79,39 @@ public IActionResult Item(int id = -1)
return View(itemModel);
}
+ public IActionResult AddItem(){
+ ViewBag.PageContentHeader = "New Item";
+
+ ViewBag.ItemCategories = Models.SelectListItemHelper.ItemCategories();
+ ViewBag.Measurements = Models.SelectListItemHelper.UnitOfMeasurements();
+ ViewBag.ItemTaxGroups = Models.SelectListItemHelper.ItemTaxGroups();
+ ViewBag.PreferredVendorId = Models.SelectListItemHelper.Vendors();
+ ViewBag.Accounts = Models.SelectListItemHelper.Accounts();
+
+ Item itemModel = new Item();
+
+ return View(itemModel);
+ }
+
+ [HttpPost]
+ public IActionResult AddItem(Item itemModel){
+ ViewBag.PageContentHeader = "New Item";
+
+ if (ModelState.IsValid) {
+ _logger.LogInformation("Item Model is Valid: " + itemModel.Description);
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(itemModel);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+ var response = Post("Inventory/SaveItem", content);
+ _logger.LogInformation("Response: " + response);
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("Items");
+ }
+
+ return View(itemModel);
+ }
+
+ [HttpPost]
public IActionResult SaveItem(Item itemModel)
{
if (ModelState.IsValid)
@@ -86,7 +122,7 @@ public IActionResult SaveItem(Item itemModel)
var response = PostAsync("inventory/saveitem", content);
- return RedirectToAction("Items");
+ return RedirectToAction("Index");
}
ViewBag.Accounts = Models.SelectListItemHelper.Accounts();
@@ -94,13 +130,12 @@ public IActionResult SaveItem(Item itemModel)
ViewBag.Measurements = Models.SelectListItemHelper.UnitOfMeasurements();
ViewBag.ItemCategories = Models.SelectListItemHelper.ItemCategories();
-
if (itemModel.Id > 0)
ViewBag.PageContentHeader = "Item Item";
else
ViewBag.PageContentHeader = "New Card";
- return View("Item", itemModel);
+ return View("Index");
}
}
}
diff --git a/src/AccountGoWeb/Controllers/PurchasingController.cs b/src/AccountGoWeb/Controllers/PurchasingController.cs
index d8215259c..e51d8364e 100644
--- a/src/AccountGoWeb/Controllers/PurchasingController.cs
+++ b/src/AccountGoWeb/Controllers/PurchasingController.cs
@@ -1,16 +1,17 @@
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using System.Net.Http;
+using Dto.Purchasing;
+using Microsoft.AspNetCore.Mvc;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
+ //[Microsoft.AspNetCore.Authorization.Authorize]
public class PurchasingController : BaseController
{
- public PurchasingController(IConfiguration config)
+ private readonly ILogger _logger;
+ public PurchasingController(IConfiguration config, ILogger logger)
{
_baseConfig = config;
Models.SelectListItemHelper._config = config;
+ _logger = logger;
}
public IActionResult Index()
@@ -24,7 +25,7 @@ public IActionResult PurchaseOrders()
string purchaseOrders = GetAsync("purchasing/purchaseorders")
.Result
- .ToString();
+ .ToString()!;
return View(model: purchaseOrders);
}
@@ -32,21 +33,106 @@ public IActionResult PurchaseOrders()
public IActionResult AddPurchaseOrder()
{
ViewBag.PageContentHeader = "Add Purchase Order";
+ PurchaseOrder purchaseOrderModel = new PurchaseOrder();
+ purchaseOrderModel.PurchaseOrderLines = new List { new PurchaseOrderLine {
+ Amount = 0,
+ Discount = 0,
+ ItemId = 1,
+ Quantity = 1,
+ } };
+ purchaseOrderModel.No = new System.Random().Next(1, 99999).ToString();
ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
- return View();
+ return View(purchaseOrderModel);
+ }
+
+ [HttpPost]
+ public IActionResult AddPurchaseOrder(PurchaseOrder purchaseOrder, string addRowBtn)
+ {
+ ViewBag.PageContentHeader = "Add Purchase Order";
+
+ if (!string.IsNullOrEmpty(addRowBtn))
+ {
+ purchaseOrder.PurchaseOrderLines.Add(new PurchaseOrderLine
+ {
+ Amount = 0,
+ Discount = 0,
+ ItemId = 1,
+ Quantity = 1
+ });
+
+ ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(purchaseOrder);
+ }
+ else if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(purchaseOrder);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ var response = PostAsync("purchasing/savepurchaseorder", content);
+
+ return RedirectToAction("PurchaseOrders");
+ }
+
+ return View("PurchaseOrders");
+ }
+
+ public IActionResult PurchaseInvoice(int id)
+ {
+ ViewBag.PageContentHeader = "Purchase Invoice";
+
+ PurchaseInvoice? purchaseInvoiceModel = null;
+
+ if (id == 0)
+ {
+ ViewBag.PageContentHeader = "New Purchase Invoice";
+ return View("PurchaseInvoice");
+ }
+ else
+ {
+ purchaseInvoiceModel = GetAsync("Purchasing/PurchaseInvoice?id=" + id).Result;
+ }
+
+ ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(purchaseInvoiceModel);
}
- public IActionResult PurchaseOrder(int purchId = 0)
+
+ public IActionResult PurchaseOrder(int id)
{
ViewBag.PageContentHeader = "Purchase Order";
- var purchOrderDto = GetAsync("purchasing/purchaseorder?id=" + purchId).Result;
+ PurchaseOrder? purchaseOrderModel = null;
+
+ if (id == 0)
+ {
+ ViewBag.PageContentHeader = "New Purchase Order";
+ return View();
+ }
+ else
+ {
+ purchaseOrderModel = GetAsync("Purchasing/PurchaseOrder?id=" + id).Result;
+ }
ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
- return View();
+ return View(purchaseOrderModel);
}
public async System.Threading.Tasks.Task PurchaseInvoices()
@@ -54,8 +140,8 @@ public async System.Threading.Tasks.Task PurchaseInvoices()
ViewBag.PageContentHeader = "Purchase Invoices";
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 + "purchasing/purchaseinvoices");
if (response.IsSuccessStatusCode)
@@ -67,18 +153,59 @@ public async System.Threading.Tasks.Task PurchaseInvoices()
return View();
}
- public IActionResult AddPurchaseInvoice(int purchId = 0)
+ public IActionResult AddPurchaseInvoice()
{
ViewBag.PageContentHeader = "New Invoice";
- return View();
+ PurchaseInvoice purchaseInvoiceModel = new PurchaseInvoice();
+ purchaseInvoiceModel.PurchaseInvoiceLines = new List { new PurchaseInvoiceLine {
+ Amount = 0,
+ Discount = 0,
+ ItemId = 1,
+ Quantity = 1,
+ } };
+ purchaseInvoiceModel.No = new System.Random().Next(1, 99999).ToString();
+
+ ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(purchaseInvoiceModel);
}
- public IActionResult PurchaseInvoice(int id)
+ [HttpPost]
+ public async System.Threading.Tasks.Task AddPurchaseInvoice(PurchaseInvoice purchaseInvoice, string addRowBtn)
{
- ViewBag.PageContentHeader = "Purchase Invoice";
+ ViewBag.PageContentHeader = "New Invoice";
+ if (!string.IsNullOrEmpty(addRowBtn))
+ {
+ purchaseInvoice.PurchaseInvoiceLines.Add(new PurchaseInvoiceLine
+ {
+ Amount = 0,
+ Discount = 0,
+ ItemId = 1,
+ Quantity = 1
+ });
- ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.Vendors = Models.SelectListItemHelper.Vendors();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(purchaseInvoice);
+ }
+ else if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(purchaseInvoice);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ var response = await PostAsync("Purchasing/SavePurchaseInvoice", content);
+ _logger.LogInformation("Purchase Invoice Saved" + purchaseInvoice.Id);
+
+ return RedirectToAction("PurchaseInvoices");
+ }
return View();
}
@@ -95,8 +222,8 @@ public async System.Threading.Tasks.Task Vendors()
ViewBag.PageContentHeader = "Vendors";
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 + "purchasing/vendors");
if (response.IsSuccessStatusCode)
@@ -109,7 +236,7 @@ public async System.Threading.Tasks.Task Vendors()
}
public IActionResult Vendor(int id = -1)
{
- Dto.Purchasing.Vendor vendorModel = null;
+ Dto.Purchasing.Vendor? vendorModel = null;
if (id == -1)
{
ViewBag.PageContentHeader = "New Vendor";
@@ -169,7 +296,7 @@ public IActionResult Payment(int id)
VendorId = invoice.VendorId,
VendorName = invoice.VendorName,
InvoiceAmount = invoice.Amount,
- AmountPaid = invoice.AmountPaid,
+ AmountPaid = invoice.AmountPaid,
Date = invoice.InvoiceDate
};
@@ -177,7 +304,7 @@ public IActionResult Payment(int id)
return View(model);
}
-
+
[HttpPost]
public IActionResult Payment(Models.Purchasing.Payment model)
{
diff --git a/src/AccountGoWeb/Controllers/QuotationsController.cs b/src/AccountGoWeb/Controllers/QuotationsController.cs
index cbe402648..85174e4b1 100644
--- a/src/AccountGoWeb/Controllers/QuotationsController.cs
+++ b/src/AccountGoWeb/Controllers/QuotationsController.cs
@@ -1,14 +1,17 @@
-using System.Net.Http;
+using Dto.Sales;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
- public class QuotationsController : BaseController
+ //[Microsoft.AspNetCore.Authorization.Authorize]
+ public class QuotationsController : GoodController
{
- public QuotationsController(IConfiguration config) {
- _baseConfig = config;
+ //private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+ public QuotationsController(IConfiguration config, ILogger logger)
+ {
+ _configuration = config;
+ _logger = logger;
}
public IActionResult Index()
@@ -22,8 +25,8 @@ public async System.Threading.Tasks.Task Quotations()
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = await client.GetAsync(baseUri + "sales/quotations");
if (response.IsSuccessStatusCode)
@@ -36,18 +39,106 @@ public async System.Threading.Tasks.Task Quotations()
return View();
}
+ [HttpGet]
public IActionResult AddSalesQuotation()
{
ViewBag.PageContentHeader = "Add Sales Quotation";
- return View();
+ SalesQuotation model = new SalesQuotation();
+ model.SalesQuotationLines = new List { new SalesQuotationLine {
+ Amount = 0,
+ Quantity = 1,
+ Discount = 0,
+ ItemId = 1,
+ MeasurementId = 1,
+ } };
+ model.No = new System.Random().Next(1, 99999).ToString(); // TODO: Replace with system generated numbering.
+
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(model);
}
- public IActionResult Quotation()
+ [HttpPost]
+ public async Task AddSalesQuotation(Dto.Sales.SalesQuotation model, string? addRowBtn)
{
- ViewBag.PageContentHeader = "Sales Quotation";
+ if (!string.IsNullOrEmpty(addRowBtn))
+ {
+ _logger.LogInformation("Add Row Button Clicked");
+ model.SalesQuotationLines.Add(new SalesQuotationLine
+ {
+ Amount = 0,
+ Quantity = 1,
+ Discount = 0,
+ ItemId = 1,
+ MeasurementId = 1,
+ });
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
- return View();
+ return View(model);
+
+ }
+ else if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(model);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ using (var client = new HttpClient())
+ {
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new Uri(baseUri!);
+ var response = await client.PostAsync("sales/savequotation", content);
+
+ if (response.IsSuccessStatusCode) {
+ _logger.LogInformation("Quotation has been successfully saved.");
+ } else {
+ _logger.LogInformation("Quotation save failed.");
+ }
+ return RedirectToAction("quotations");
+ }
+ } else {
+ _logger.LogInformation("Model State is not valid.");
+ return RedirectToAction("quotations");
+ }
+
+ }
+
+ [HttpGet]
+ public IActionResult Quotation(int id)
+ {
+ ViewBag.PageContentHeader = "Sales";
+
+ SalesQuotation? model = null;
+
+ if (id == 0)
+ {
+ ViewBag.PageContentHeader = "Add Sales Quotation";
+ return View("AddSalesQuotation");
+ }
+ else
+ {
+ model = GetAsync("Sales/Quotation?id=" + id).Result;
+ @ViewBag.Id = model.Id;
+ @ViewBag.QuotationDate = model.QuotationDate;
+ @ViewBag.CustomerName = model.CustomerName;
+ @ViewBag.PaymentTermId = model.PaymentTermId;
+ @ViewBag.SalesQuotationLines = model.SalesQuotationLines;
+ @ViewBag.TotalAmount = model.Amount;
+ }
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(model);
}
}
}
diff --git a/src/AccountGoWeb/Controllers/SPAProxyController.cs b/src/AccountGoWeb/Controllers/SPAProxyController.cs
index 67313350c..b17b3bbd4 100644
--- a/src/AccountGoWeb/Controllers/SPAProxyController.cs
+++ b/src/AccountGoWeb/Controllers/SPAProxyController.cs
@@ -1,10 +1,7 @@
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using System.Text;
namespace AccountGoWeb.Controllers
{
diff --git a/src/AccountGoWeb/Controllers/SalesController.cs b/src/AccountGoWeb/Controllers/SalesController.cs
index ec582bf86..520edf5ad 100644
--- a/src/AccountGoWeb/Controllers/SalesController.cs
+++ b/src/AccountGoWeb/Controllers/SalesController.cs
@@ -1,19 +1,21 @@
using AccountGoWeb.Models;
using Dto.Sales;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using System.Collections.Generic;
-using System.Net.Http;
+using Newtonsoft.Json;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
- public class SalesController : BaseController
+ //[Microsoft.AspNetCore.Authorization.Authorize]
+ public class SalesController : GoodController
{
- public SalesController(IConfiguration config)
+ // private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+
+ public SalesController(IConfiguration config, ILogger logger)
{
- _baseConfig = config;
+ _configuration = config;
Models.SelectListItemHelper._config = config;
+ _logger = logger;
}
public IActionResult Index()
@@ -26,8 +28,8 @@ public async System.Threading.Tasks.Task SalesOrders()
ViewBag.PageContentHeader = "Sales Orders";
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = await client.GetAsync(baseUri + "sales/salesorders");
if (response.IsSuccessStatusCode)
@@ -42,26 +44,135 @@ public async System.Threading.Tasks.Task SalesOrders()
public IActionResult AddSalesOrder()
{
ViewBag.PageContentHeader = "Add Sales Order";
-
- return View();
+ SalesOrder salesOrderModel = new SalesOrder();
+ salesOrderModel.SalesOrderLines = new List { new SalesOrderLine {
+ Amount = 0,
+ Discount = 0,
+ ItemId = 1,
+ Quantity = 1,
+ } };
+ salesOrderModel.No = new System.Random().Next(1, 99999).ToString();
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(salesOrderModel);
}
[HttpPost]
- public IActionResult AddSalesOrder(object Dto)
+ public IActionResult AddSalesOrder(SalesOrder Dto, string addRowBtn)
{
- return Ok();
+ if (!string.IsNullOrEmpty(addRowBtn))
+ {
+ Dto.SalesOrderLines.Add(new SalesOrderLine
+ {
+ Amount = 0,
+ Quantity = 1,
+ Discount = 0,
+ ItemId = 1,
+ MeasurementId = 1,
+ });
+
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(Dto);
+ }
+ else if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(Dto);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ var response = Post("Sales/addsalesorder", content);
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("salesorders");
+ }
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return RedirectToAction("salesorders");
}
-
+
public IActionResult SalesOrder(int id)
{
ViewBag.PageContentHeader = "Sales Order";
- return View();
+ SalesOrder? salesOrderModel = null;
+ if (id == -1)
+ {
+ ViewBag.PageContentHeader = "Add Sales Order";
+ return View("AddSalesOrder");
+
+ }
+ else
+ {
+ try
+ {
+ salesOrderModel = GetAsync("Sales/SalesOrder?id=" + id).Result;
+ if (salesOrderModel == null)
+ {
+ ViewBag.ErrorMessage = "Sales order not found. Please check the order ID and try again.";
+ _logger.LogWarning("Sales order with id {Id} returned null from API", id);
+ return View(salesOrderModel);
+ }
+ ViewBag.CustomerName = salesOrderModel.CustomerName ?? "Unknown";
+ ViewBag.OrderDate = salesOrderModel.OrderDate;
+ ViewBag.SalesOrderLines = salesOrderModel.SalesOrderLines ?? new List();
+ ViewBag.TotalAmount = salesOrderModel.Amount;
+ }
+ catch (HttpRequestException hre)
+ {
+ _logger.LogError(hre, "HTTP error loading sales order with id {Id}", id);
+ ViewBag.ErrorMessage = $"Failed to load sales order. Status: {hre.Message}";
+ return View(salesOrderModel);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected error loading sales order with id {Id}", id);
+ ViewBag.ErrorMessage = "An unexpected error occurred while loading the sales order. Please try again later.";
+ return View(salesOrderModel);
+ }
+ }
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(salesOrderModel);
}
public IActionResult SalesInvoice(int id)
{
ViewBag.PageContentHeader = "Sales Invoice";
- return View();
+ SalesInvoice? salesInvoiceModel = null;
+
+ if (id == 0)
+ {
+ ViewBag.PageContentHeader = "Add Sales Invoice";
+ return View("AddSalesInvoice");
+ }
+ else
+ {
+ salesInvoiceModel = GetAsync("Sales/SalesInvoice?id=" + id).Result;
+ ViewBag.Id = salesInvoiceModel.Id;
+ ViewBag.CustomerName = salesInvoiceModel.CustomerName;
+ ViewBag.InvoiceDate = salesInvoiceModel.InvoiceDate;
+ ViewBag.SalesInvoiceLines = salesInvoiceModel.SalesInvoiceLines;
+ ViewBag.TotalAmount = salesInvoiceModel.Amount;
+ }
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View("SalesInvoice", salesInvoiceModel);
}
public async System.Threading.Tasks.Task SalesInvoices()
@@ -69,8 +180,8 @@ public async System.Threading.Tasks.Task SalesInvoices()
ViewBag.PageContentHeader = "Sales Invoices";
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = await client.GetAsync(baseUri + "sales/salesinvoices");
if (response.IsSuccessStatusCode)
@@ -78,47 +189,170 @@ public async System.Threading.Tasks.Task SalesInvoices()
var responseJson = await response.Content.ReadAsStringAsync();
return View(model: responseJson);
}
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
}
return View();
}
+ [HttpPost]
+ public async System.Threading.Tasks.Task SalesInvoice(SalesInvoice salesInvoiceModel)
+ {
+ if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(salesInvoiceModel);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+ string ReadAsStringAsync = await content.ReadAsStringAsync();
+ _logger.LogInformation("SaveSalesInvoice: " + ReadAsStringAsync);
+ var response = Post("Sales/UpdateSalesInvoice", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ return RedirectToAction("SalesInvoices");
+ }
+ }
+
+ ViewBag.Customers = SelectListItemHelper.Customers();
+ ViewBag.PaymentTerms = SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = SelectListItemHelper.Items();
+ ViewBag.Measurements = SelectListItemHelper.Measurements();
+ ViewBag.TotalAmount = salesInvoiceModel.Amount;
+
+ return View(salesInvoiceModel);
+ }
+
+ [HttpGet]
public IActionResult AddSalesInvoice()
{
ViewBag.PageContentHeader = "Add Sales Invoice";
- return View();
+ SalesInvoice salesInvoiceModel = new SalesInvoice();
+ salesInvoiceModel.SalesInvoiceLines = new List { new SalesInvoiceLine {
+ Amount = 0,
+ Discount = 0,
+ ItemId = 1,
+ Quantity = 1,
+ } };
+ salesInvoiceModel.No = new System.Random().Next(1, 99999).ToString(); // TODO: Replace with system generated numbering.
+
+ @ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ @ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ @ViewBag.Items = Models.SelectListItemHelper.Items();
+ @ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(salesInvoiceModel);
+ }
+
+ [HttpPost]
+ public async System.Threading.Tasks.Task AddSalesInvoice(SalesInvoice Dto, string? addRowBtn)
+ {
+ if (!string.IsNullOrEmpty(addRowBtn))
+ {
+ Dto.SalesInvoiceLines!.Add(new SalesInvoiceLine
+ {
+ Amount = 0,
+ Quantity = 1,
+ Discount = 0,
+ ItemId = 1,
+ MeasurementId = 1,
+ });
+
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ 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("AddSalesInvoice: " + await content.ReadAsStringAsync());
+ var response = Post("Sales/CreateSalesInvoice", content);
+
+ _logger.LogInformation("AddSalesInvoice response: " + response.ToString());
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("salesinvoices");
+ }
+
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.Items = Models.SelectListItemHelper.Items();
+ ViewBag.PaymentTerms = Models.SelectListItemHelper.PaymentTerms();
+ ViewBag.Measurements = Models.SelectListItemHelper.Measurements();
+
+ return View(Dto);
}
public async System.Threading.Tasks.Task SalesReceipts()
{
ViewBag.PageContentHeader = "Sales Receipts";
- using (var client = new HttpClient())
+ try
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
- client.DefaultRequestHeaders.Accept.Clear();
- var response = await client.GetAsync(baseUri + "sales/salesreceipts");
- if (response.IsSuccessStatusCode)
+ using (var client = new HttpClient())
{
- var responseJson = await response.Content.ReadAsStringAsync();
- return View(model: responseJson);
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new Uri(baseUri!);
+ client.DefaultRequestHeaders.Accept.Clear();
+
+ var response = await client.GetAsync("sales/salesreceipts");
+ if (response.IsSuccessStatusCode)
+ {
+ var responseJson = await response.Content.ReadAsStringAsync();
+ return View(model: responseJson);
+ }
+ else
+ {
+ _logger.LogError("Failed to fetch sales receipts. API returned status code: {StatusCode}", response.StatusCode);
+ ViewBag.ErrorMessage = "Failed to load sales receipts. Please try again later.";
+ }
}
}
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred while fetching sales receipts.");
+ ViewBag.ErrorMessage = "An unexpected error occurred while loading sales receipts.";
+ }
+
+ // Return the view with an error message if the API call fails
+ return View(model: "[]");
+ }
+
+ [HttpGet]
+ public IActionResult SalesReceipt(int id)
+ {
return View();
}
+ [HttpGet]
public IActionResult AddReceipt()
{
- ViewBag.PageContentHeader = "New Receipt";
-
- var model = new Models.Sales.AddReceipt();
-
- ViewBag.Customers = Models.SelectListItemHelper.Customers();
- ViewBag.DebitAccounts = Models.SelectListItemHelper.CashBanks();
- ViewBag.CreditAccounts = Models.SelectListItemHelper.Accounts();
- ViewBag.CustomersDetail = Newtonsoft.Json.JsonConvert.SerializeObject(GetAsync>("sales/customers").Result);
-
- return View(model);
+ try
+ {
+ ViewBag.PageContentHeader = "New Receipt";
+ ViewBag.Customers = Models.SelectListItemHelper.Customers();
+ ViewBag.DebitAccounts = Models.SelectListItemHelper.CashBanks();
+ ViewBag.CreditAccounts = Models.SelectListItemHelper.Accounts();
+ ViewBag.CustomersDetail = Newtonsoft.Json.JsonConvert.SerializeObject(
+ GetAsync>("sales/customers").Result
+ );
+
+ var model = new Models.Sales.AddReceipt();
+ return View(model);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred while preparing the Add Receipt page.");
+ ViewBag.ErrorMessage = "Failed to load the page for adding a receipt. Please try again later.";
+ return View(new Models.Sales.AddReceipt());
+ }
}
[HttpPost]
@@ -126,32 +360,49 @@ public IActionResult AddReceipt(Models.Sales.AddReceipt model)
{
if (ModelState.IsValid)
{
- var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(model);
- var content = new StringContent(serialize);
- content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
- var response = Post("sales/savereceipt", content);
- if(response.IsSuccessStatusCode)
- return RedirectToAction("salesreceipts");
+ try
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(model);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ var response = Post("sales/savereceipt", content);
+ if (response.IsSuccessStatusCode)
+ {
+ return RedirectToAction("SalesReceipts");
+ }
+ else
+ {
+ _logger.LogError("Failed to save receipt. API returned status code: {StatusCode}", response.StatusCode);
+ ViewBag.ErrorMessage = "Failed to save the receipt. Please try again.";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred while saving the receipt.");
+ ViewBag.ErrorMessage = "An unexpected error occurred. Please try again.";
+ }
}
+ // Reload dropdowns and return the view if validation or API call fails
ViewBag.PageContentHeader = "New Receipt";
-
ViewBag.Customers = Models.SelectListItemHelper.Customers();
ViewBag.DebitAccounts = Models.SelectListItemHelper.CashBanks();
ViewBag.CreditAccounts = Models.SelectListItemHelper.Accounts();
- ViewBag.CustomersDetail = Newtonsoft.Json.JsonConvert.SerializeObject(GetAsync>("sales/customers").Result);
+ ViewBag.CustomersDetail = Newtonsoft.Json.JsonConvert.SerializeObject(
+ GetAsync>("sales/customers").Result
+ );
return View(model);
}
-
public async System.Threading.Tasks.Task Customers()
{
ViewBag.PageContentHeader = "Customers";
using (var client = new HttpClient())
{
- var baseUri = _baseConfig["ApiUrl"];
- client.BaseAddress = new System.Uri(baseUri);
+ var baseUri = _configuration!["ApiUrl"];
+ client.BaseAddress = new System.Uri(baseUri!);
client.DefaultRequestHeaders.Accept.Clear();
var response = await client.GetAsync(baseUri + "sales/customers");
if (response.IsSuccessStatusCode)
@@ -162,10 +413,10 @@ public async System.Threading.Tasks.Task Customers()
}
return View();
}
-
+
public IActionResult Customer(int id = -1)
{
- Customer customerModel = null;
+ Customer? customerModel = null;
if (id == -1)
{
ViewBag.PageContentHeader = "New Customer";
@@ -185,24 +436,50 @@ public IActionResult Customer(int id = -1)
return View(customerModel);
}
- public IActionResult SaveCustomer(Customer customerModel)
+ [HttpPost]
+ public async System.Threading.Tasks.Task SaveSalesInvoice(SalesInvoice salesInvoiceModel)
{
if (ModelState.IsValid)
{
- var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(customerModel);
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(salesInvoiceModel);
var content = new StringContent(serialize);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
- var response = PostAsync("sales/savecustomer", content);
+ string ReadAsStringAsync = await content.ReadAsStringAsync();
+ _logger.LogInformation("SaveSalesInvoice: " + ReadAsStringAsync);
+ var response = Post("Sales/SaveSalesInvoice", content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ return RedirectToAction("SalesInvoices");
+ }
+ }
+ ViewBag.Customers = SelectListItemHelper.Customers();
+ ViewBag.PaymentTerms = SelectListItemHelper.PaymentTerms();
+ ViewBag.Items = SelectListItemHelper.Items();
+ ViewBag.Measurements = SelectListItemHelper.Measurements();
+
+ return View("SalesInvoice", salesInvoiceModel);
+ }
+ public async Task SaveCustomer(Customer customerModel)
+ {
+ if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(customerModel);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+ string ReadAsStringAsync = await content.ReadAsStringAsync();
+ var response = await PostAsync("Sales/SaveCustomer", content);
return RedirectToAction("Customers");
}
- else {
+ else
+ {
ViewBag.Accounts = SelectListItemHelper.Accounts();
ViewBag.TaxGroups = SelectListItemHelper.TaxGroups();
ViewBag.PaymentTerms = SelectListItemHelper.PaymentTerms();
}
- if(customerModel.Id == -1)
+ if (customerModel.Id == -1)
ViewBag.PageContentHeader = "New Customer";
else
ViewBag.PageContentHeader = "Customer Card";
@@ -210,6 +487,7 @@ public IActionResult SaveCustomer(Customer customerModel)
return View("Customer", customerModel);
}
+
public IActionResult CustomerAllocations(int id)
{
ViewBag.PageContentHeader = "Customer Allocations";
@@ -217,38 +495,71 @@ public IActionResult CustomerAllocations(int id)
return View();
}
+ // [HttpGet]
public IActionResult Allocate(int id)
{
- ViewBag.PageContentHeader = "Receipt Allocation";
+ Console.WriteLine($"Allocate called with ID: {id}");
+
+ try
+ {
+ ViewBag.PageContentHeader = "Sales Receipt Allocation";
- var model = new Models.Sales.Allocate();
+ var model = new Models.Sales.Allocate();
- var receipt = GetAsync("sales/salesreceipt?id=" + id).Result;
+ // Fetch receipt details
+ var receipt = GetAsync("sales/salesreceipt?id=" + id).Result;
+ if (receipt == null)
+ {
+ Console.WriteLine($"Receipt not found for ID: {id}");
+ _logger.LogError("Failed to fetch receipt with id: {id}", id);
+ return NotFound($"Receipt with id {id} not found.");
+ }
- ViewBag.CustomerName = receipt.CustomerName;
- ViewBag.ReceiptNo = receipt.ReceiptNo;
+ ViewBag.CustomerName = receipt.CustomerName;
+ ViewBag.ReceiptNo = receipt.ReceiptNo;
- model.CustomerId = receipt.CustomerId;
- model.ReceiptId = receipt.Id;
- model.Date = receipt.ReceiptDate;
- model.Amount = receipt.Amount;
- model.RemainingAmountToAllocate = receipt.RemainingAmountToAllocate;
+ model.CustomerId = receipt.CustomerId;
+ model.ReceiptId = receipt.Id;
+ model.Date = receipt.ReceiptDate;
+ model.Amount = receipt.Amount;
+ model.RemainingAmountToAllocate = receipt.RemainingAmountToAllocate;
- var invoices = GetAsync>("sales/customerinvoices?id=" + receipt.CustomerId).Result;
+ // Fetch customer invoices
+ _logger.LogInformation("Calling API: sales/customerinvoices?id={id}", receipt.CustomerId);
- foreach (var invoice in invoices) {
- if (invoice.Posted && invoice.TotalAllocatedAmount < invoice.Amount)
+ var invoices = GetAsync>("sales/customerinvoices?id=" + receipt.CustomerId).Result;
+ if (invoices == null)
{
- model.AllocationLines.Add(new Models.Sales.AllocationLine()
+ _logger.LogError("Failed to fetch invoices for customer with id: {CustomerId}", receipt.CustomerId);
+ return NotFound($"Invoices for customer with id {receipt.CustomerId} not found.");
+ }
+
+ foreach (var invoice in invoices)
+ {
+ _logger.LogInformation("Invoice: {Invoice}", JsonConvert.SerializeObject(invoice));
+ if (invoice.Posted && invoice.TotalAllocatedAmount < invoice.Amount)
{
- InvoiceId = invoice.Id,
- Amount = invoice.Amount,
- AllocatedAmount = invoice.TotalAllocatedAmount
- });
+ model.AllocationLines.Add(new Models.Sales.AllocationLine()
+ {
+ InvoiceId = invoice.Id,
+ Amount = invoice.Amount,
+ AllocatedAmount = invoice.TotalAllocatedAmount
+ });
+ }
+ else
+ {
+ _logger.LogInformation("Invoice excluded: Posted={Posted}, TotalAllocatedAmount={TotalAllocatedAmount}, Amount={Amount}",
+ invoice.Posted, invoice.TotalAllocatedAmount, invoice.Amount);
+ }
}
- }
- return View(model);
+ return View(model);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred while processing the Allocate action for id: {id}", id);
+ return StatusCode(500, "An error occurred while processing your request.");
+ }
}
[HttpPost]
@@ -256,7 +567,8 @@ public IActionResult Allocate(Models.Sales.Allocate model)
{
if (ModelState.IsValid)
{
- if (model.IsValid()) {
+ if (model.IsValid())
+ {
var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(model);
var content = new StringContent(serialize);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
@@ -288,5 +600,22 @@ public IActionResult SalesInvoicePdf(int id)
salesInvoiceModel.SalesInvoiceLines = invoice.SalesInvoiceLines;
return View(salesInvoiceModel);
}
+
+ public async Task DeleteSalesInvoice(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 + "Sales/DeleteSalesInvoice?id=" + id);
+
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("SalesInvoices");
+ }
+
+ return RedirectToAction("SalesInvoices");
+ }
+
}
}
diff --git a/src/AccountGoWeb/Controllers/TaxController.cs b/src/AccountGoWeb/Controllers/TaxController.cs
index bbc4e9132..4fd7b9a42 100644
--- a/src/AccountGoWeb/Controllers/TaxController.cs
+++ b/src/AccountGoWeb/Controllers/TaxController.cs
@@ -1,43 +1,182 @@
-using Microsoft.AspNetCore.Mvc;
+using AccountGoWeb.Models.TaxSystem;
+using AutoMapper;
+using Dto.TaxSystem;
+using Microsoft.AspNetCore.Mvc;
+using System;
namespace AccountGoWeb.Controllers
{
- [Microsoft.AspNetCore.Authorization.Authorize]
+ //[Microsoft.AspNetCore.Authorization.Authorize]
public class TaxController : BaseController
{
- public TaxController(Microsoft.Extensions.Configuration.IConfiguration config)
+ private readonly IMapper _mapper;
+
+ public TaxController(Microsoft.Extensions.Configuration.IConfiguration config, IMapper mapper)
{
_baseConfig = config;
+ _mapper = mapper;
}
- public IActionResult Index() {
+ public IActionResult Index()
+ {
return RedirectToAction("taxes");
}
- public async System.Threading.Tasks.Task Taxes()
+ public async Task Taxes()
{
ViewBag.PageContentHeader = "Tax";
using (var client = new System.Net.Http.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 + "tax/taxes");
if (response.IsSuccessStatusCode)
{
var responseJson = await response.Content.ReadAsStringAsync();
- var taxSystemDto = Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson);
- var taxSystemViewModel = new Models.TaxSystem.TaxSystemViewModel();
- taxSystemViewModel.Taxes = taxSystemDto.Taxes;
- taxSystemViewModel.ItemTaxGroups = taxSystemDto.ItemTaxGroups;
- taxSystemViewModel.TaxGroups = taxSystemDto.TaxGroups;
+ var taxSystemDto = Newtonsoft.Json.JsonConvert.DeserializeObject(responseJson);
+ var taxSystemViewModel = _mapper.Map(taxSystemDto);
+
return View(taxSystemViewModel);
}
}
return View();
}
+
+ public IActionResult AddNewTax()
+ {
+ ViewBag.PageContentHeader = "Add New Tax";
+
+ @ViewBag.TaxGroups = Models.SelectListItemHelper.TaxGroups();
+ @ViewBag.ItemTaxGroups = Models.SelectListItemHelper.ItemTaxGroups();
+
+ return View();
+ }
+
+ [HttpPost]
+ public IActionResult AddNewTax(TaxForCreation taxForCreationDto)
+ {
+ if (ModelState.IsValid)
+ {
+ var serialize = Newtonsoft.Json.JsonConvert.SerializeObject(taxForCreationDto);
+ var content = new StringContent(serialize);
+ content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+ var response = Post("Tax/addnewtax", content);
+ if (response.IsSuccessStatusCode)
+ return RedirectToAction("Taxes");
+ }
+
+ @ViewBag.TaxGroups = Models.SelectListItemHelper.TaxGroups();
+ @ViewBag.ItemTaxGroups = Models.SelectListItemHelper.ItemTaxGroups();
+
+ return View();
+ }
+
+ public IActionResult EditTax(string tax, string taxGroup, string itemTaxGroup)
+ {
+ ViewBag.PageContentHeader = "Edit Tax";
+
+ // Mapping Dto to View Model
+ var taxObj = Newtonsoft.Json.JsonConvert.DeserializeObject(tax);
+ var taxGroupObj = Newtonsoft.Json.JsonConvert.DeserializeObject(taxGroup);
+ var itemTaxGroupObj = Newtonsoft.Json.JsonConvert.DeserializeObject