diff --git a/docs/README.md b/docs/README.md index 8fcac83..96c0a16 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,5 @@ # Property Portfolio Manager + A website for managing a number of rental properties. This project is work-in-porgress. @@ -10,13 +11,28 @@ The C# aspnetcore application is intended for use by landlords to manage a portf The User interface is written as a Blazor Webassembly application hosted in a C# Web API project. ## Data access + The Web API uses service and repository layers to access a SQL Server database. ## Authentication + TODO ## Document & Image storage - MS Graph + TODO ## Finance - Double entry bookkeeping + TODO + +## Other packages used + +### Importing bank statements + +The importing of bank statement csv files is completed using [CsvHelper](https://github.com/JoshClose/CsvHelper). +The initial implementation during development uses hard-coded column mappings and CultureInfo. This will eventually be stored against the individual portfolios. + +### Caching + +Redis is used for caching and the [DRJTechnology.Cache](https://github.com/DRJTechnology/DRJTechnology.Cache) package is used to implement this. diff --git a/src/PropertyPortfolioManager.Client/Interfaces/IBankStatementService.cs b/src/PropertyPortfolioManager.Client/Interfaces/IBankStatementService.cs new file mode 100644 index 0000000..c692dcd --- /dev/null +++ b/src/PropertyPortfolioManager.Client/Interfaces/IBankStatementService.cs @@ -0,0 +1,9 @@ +using PropertyPortfolioManager.Models.Model.Document; + +namespace PropertyPortfolioManager.Client.Interfaces +{ + public interface IBankStatementService : IGenericDataService + { + Task UploadBankStatement(Stream content, string filename); + } +} diff --git a/src/PropertyPortfolioManager.Client/Pages/UploadBankStatement.razor b/src/PropertyPortfolioManager.Client/Pages/UploadBankStatement.razor new file mode 100644 index 0000000..069d2a1 --- /dev/null +++ b/src/PropertyPortfolioManager.Client/Pages/UploadBankStatement.razor @@ -0,0 +1,21 @@ +@page "/uploadstatement" + + +
+
+
Upload bank statement
+
+
+
+ +
+ +
+ +@if (file != null) +{ +
+

File Name: @file.Name

+

File Size: @file.Size bytes

+
+} diff --git a/src/PropertyPortfolioManager.Client/Pages/UploadBankStatement.razor.cs b/src/PropertyPortfolioManager.Client/Pages/UploadBankStatement.razor.cs new file mode 100644 index 0000000..3210380 --- /dev/null +++ b/src/PropertyPortfolioManager.Client/Pages/UploadBankStatement.razor.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using PropertyPortfolioManager.Client.Interfaces; + +namespace PropertyPortfolioManager.Client.Pages +{ + public partial class UploadBankStatement + { + [Inject] + public IBankStatementService bankStatementService { get; set; } + + private IBrowserFile file; + + private async Task HandleFileSelection(InputFileChangeEventArgs e) + { + file = e.File; + var stream = file.OpenReadStream(); + await bankStatementService.UploadBankStatement(stream, file.Name); + } + } +} diff --git a/src/PropertyPortfolioManager.Client/Program.cs b/src/PropertyPortfolioManager.Client/Program.cs index 7f5d798..6aa824c 100644 --- a/src/PropertyPortfolioManager.Client/Program.cs +++ b/src/PropertyPortfolioManager.Client/Program.cs @@ -16,26 +16,27 @@ // Supply HttpClient instances that include access tokens when making requests to the server project builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("PropertyPortfolioManager.ServerAPI")); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMsalAuthentication(options => { builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); options.ProviderOptions.DefaultAccessTokenScopes.Add("api://2956eaa1-1d62-448d-9ac3-58ea18e4f302/API.Access"); -options.ProviderOptions.LoginMode = "Redirect"; + options.ProviderOptions.LoginMode = "Redirect"; }); await builder.Build().RunAsync(); diff --git a/src/PropertyPortfolioManager.Client/Services/BankStatementService.cs b/src/PropertyPortfolioManager.Client/Services/BankStatementService.cs new file mode 100644 index 0000000..785a649 --- /dev/null +++ b/src/PropertyPortfolioManager.Client/Services/BankStatementService.cs @@ -0,0 +1,26 @@ +using PropertyPortfolioManager.Client.Interfaces; + +namespace PropertyPortfolioManager.Client.Services +{ + public class BankStatementService : GenericDataService, IBankStatementService + { + public BankStatementService(HttpClient httpClient) + : base(httpClient) + { + ApiControllerName = "BankStatement"; + } + + public async Task UploadBankStatement(Stream content, string filename) + { + var formContent = new MultipartFormDataContent(); + formContent.Add(new StreamContent(content), "File", filename); + + var response = await httpClient.PostAsync($"api/BankStatement/UploadBankStatement", formContent); + + if (response == null || !response.IsSuccessStatusCode) + { + throw new Exception($"Failed to upload bank statement."); + } + } + } +} diff --git a/src/PropertyPortfolioManager.Client/Shared/NavMenu.razor b/src/PropertyPortfolioManager.Client/Shared/NavMenu.razor index f80810a..df90616 100644 --- a/src/PropertyPortfolioManager.Client/Shared/NavMenu.razor +++ b/src/PropertyPortfolioManager.Client/Shared/NavMenu.razor @@ -51,6 +51,11 @@ Bank Accounts +
  • + + Upload Bank Statement + +
  • Accounts diff --git a/src/PropertyPortfolioManager.Database/PropertyPortfolioManager.Database.sqlproj b/src/PropertyPortfolioManager.Database/PropertyPortfolioManager.Database.sqlproj index 53733d7..25cccb5 100644 --- a/src/PropertyPortfolioManager.Database/PropertyPortfolioManager.Database.sqlproj +++ b/src/PropertyPortfolioManager.Database/PropertyPortfolioManager.Database.sqlproj @@ -74,14 +74,18 @@ + + + + @@ -157,6 +161,7 @@ + diff --git a/src/PropertyPortfolioManager.Database/Scripts/Transactions with running balance.sql b/src/PropertyPortfolioManager.Database/Scripts/Transactions with running balance.sql new file mode 100644 index 0000000..fd9b29a --- /dev/null +++ b/src/PropertyPortfolioManager.Database/Scripts/Transactions with running balance.sql @@ -0,0 +1,52 @@ + +DECLARE + @PortfolioId int, + @FromDate datetime = null, + @ToDate datetime = null, + @AccountId int = null, + @TransactionTypeId int = null + +SET @PortfolioId = 2 +SET @FromDate = '01 Sep 2023' +SET @ToDate = '01 Oct 2023' +SET @AccountId = 1020 + +DECLARE @StartingBalance MONEY +IF (@FromDate IS NOT NULL) +BEGIN + SELECT @StartingBalance = SUM(amount * direction) + FROM finance.TransactionDetail td + INNER JOIN finance.[Transaction] t ON td.TransactionId = t.Id + INNER JOIN finance.[TransactionType] tt ON t.TransactionTypeId = tt.Id + INNER JOIN finance.Account a on td.AccountId = a.Id + WHERE t.PortfolioId = @PortfolioId + AND (td.[Date] < @FromDate) + AND (ISNULL(@AccountId, 0) = 0 OR td.AccountId = @AccountId) + AND (ISNULL(@TransactionTypeId, 0) = 0 OR t.TransactionTypeId = @TransactionTypeId) +END + + +SELECT @StartingBalance AS StartingBalance + + SELECT td.Id, + td.TransactionId, + -- tt.[Type], + td.[Date], + t.Reference, + -- td.AccountId, + -- a.[Name] AS Account, + td.[Description], + CASE WHEN direction = 1 THEN td.amount ELSE 0 END AS Debit, + CASE WHEN direction = -1 THEN td.amount ELSE 0 END AS Credit + , (SUM(td.amount * td.direction) OVER (ORDER BY td.[Date]) + @StartingBalance) * -1 AS running_total + FROM finance.TransactionDetail td + INNER JOIN finance.[Transaction] t ON td.TransactionId = t.Id + -- INNER JOIN finance.[TransactionType] tt ON t.TransactionTypeId = tt.Id + -- INNER JOIN finance.Account a on td.AccountId = a.Id + WHERE t.PortfolioId = @PortfolioId + AND (@FromDate IS NULL OR td.[Date] >= @FromDate) + AND (@ToDate IS NULL OR td.[Date] < DATEADD(DAY, 1, @ToDate)) + AND (ISNULL(@AccountId, 0) = 0 OR td.AccountId = @AccountId) + AND (ISNULL(@TransactionTypeId, 0) = 0 OR t.TransactionTypeId = @TransactionTypeId) + ORDER BY td.[Date] --DESC--, td.TransactionId, a.[Name], Credit, Debit + diff --git a/src/PropertyPortfolioManager.Database/Stored Procedures/finance/BankStatement_Upload.sql b/src/PropertyPortfolioManager.Database/Stored Procedures/finance/BankStatement_Upload.sql new file mode 100644 index 0000000..0550d59 --- /dev/null +++ b/src/PropertyPortfolioManager.Database/Stored Procedures/finance/BankStatement_Upload.sql @@ -0,0 +1,16 @@ +-- ========================================================== +-- Author: Dave Brown +-- Create date: 12 Dec 2023 +-- Description: Creates an account record +-- ========================================================== +CREATE PROCEDURE [finance].[BankStatement_Upload] + @AccountId INT, + @Statement finance.StatementTableType READONLY, + @CurrentUserId INT +AS + INSERT INTO [finance].[BankAccountDetail] (AccountId, Date, Amount, Description, TransactionType, Deleted, CreateUserId, CreateDate, AmendUserId, AmendDate) + SELECT @AccountId, Date, Amount, Description, TransactionType, 0, @CurrentUserId, SYSDATETIME(), @CurrentUserId, SYSDATETIME() + FROM @Statement + +RETURN 0 + diff --git a/src/PropertyPortfolioManager.Database/Tables/finance/BankAccountDetail.sql b/src/PropertyPortfolioManager.Database/Tables/finance/BankAccountDetail.sql new file mode 100644 index 0000000..2260cff --- /dev/null +++ b/src/PropertyPortfolioManager.Database/Tables/finance/BankAccountDetail.sql @@ -0,0 +1,16 @@ +CREATE TABLE [finance].[BankAccountDetail] +( + [Id] INT IDENTITY NOT NULL, + [AccountId] INT NOT NULL, + [Date] DATETIME NOT NULL, + [Amount] MONEY NOT NULL, + [Description] NVARCHAR(512) NOT NULL, + [TransactionType] NVARCHAR(512) NOT NULL, + [Deleted] BIT DEFAULT (0) NOT NULL, + [CreateUserId] INT NOT NULL, + [CreateDate] DATETIME CONSTRAINT [DF_BankAccountDetail_CreateDate] DEFAULT (getutcdate()) NOT NULL, + [AmendUserId] INT NOT NULL, + [AmendDate] DATETIME CONSTRAINT [DF_BankAccountDetail_AmendDate] DEFAULT (getutcdate()) NOT NULL, + CONSTRAINT [PK_BankAccountDetail] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_BankAccountDetail_Account] FOREIGN KEY ([AccountId]) REFERENCES [finance].[Account]([Id]) +) diff --git a/src/PropertyPortfolioManager.Database/Types/StatementTableType.sql.sql b/src/PropertyPortfolioManager.Database/Types/StatementTableType.sql.sql new file mode 100644 index 0000000..d392b13 --- /dev/null +++ b/src/PropertyPortfolioManager.Database/Types/StatementTableType.sql.sql @@ -0,0 +1,11 @@ +-- ========================================================== +-- Author: Dave Brown +-- Create date: 12 Dec 2023 +-- Description: Creates a Statement Table Type +-- ========================================================== +CREATE TYPE finance.StatementTableType AS TABLE ( + [Date] DATETIME NULL, + [Amount] MONEY NULL, + [Description] NVARCHAR(512) NULL, + [TransactionType] NVARCHAR(512) NULL +); diff --git a/src/PropertyPortfolioManager.Models/InternalObjects/UploadResult.cs b/src/PropertyPortfolioManager.Models/InternalObjects/UploadResult.cs new file mode 100644 index 0000000..0712a31 --- /dev/null +++ b/src/PropertyPortfolioManager.Models/InternalObjects/UploadResult.cs @@ -0,0 +1,11 @@ + +namespace PropertyPortfolioManager.Models.InternalObjects +{ + public class UploadResult + { + public bool Uploaded { get; set; } + public string? FileName { get; set; } + public string? StoredFileName { get; set; } + public int ErrorCode { get; set; } + } +} diff --git a/src/PropertyPortfolioManager.Models/Model/Finance/BankStatementMap.cs b/src/PropertyPortfolioManager.Models/Model/Finance/BankStatementMap.cs new file mode 100644 index 0000000..70dc3c8 --- /dev/null +++ b/src/PropertyPortfolioManager.Models/Model/Finance/BankStatementMap.cs @@ -0,0 +1,15 @@ +using CsvHelper.Configuration; + +namespace PropertyPortfolioManager.Models.Model.Finance +{ + public class BankStatementMap : ClassMap + { + public BankStatementMap() + { + Map(m => m.Date).Name("Date"); + Map(m => m.Amount).Name("Amount"); + Map(m => m.Description).Name("Memo"); + Map(m => m.TransactionType).Name("Subcategory"); + } + } +} \ No newline at end of file diff --git a/src/PropertyPortfolioManager.Models/Model/Finance/BankStatementModel.cs b/src/PropertyPortfolioManager.Models/Model/Finance/BankStatementModel.cs new file mode 100644 index 0000000..09aa23c --- /dev/null +++ b/src/PropertyPortfolioManager.Models/Model/Finance/BankStatementModel.cs @@ -0,0 +1,10 @@ +namespace PropertyPortfolioManager.Models.Model.Finance +{ + public class BankStatementModel + { + public DateTime Date { get; set; } + public decimal Amount { get; set; } + public string Description { get; set; } = string.Empty; + public string TransactionType { get; set; } = string.Empty; + } +} diff --git a/src/PropertyPortfolioManager.Models/PropertyPortfolioManager.Models.csproj b/src/PropertyPortfolioManager.Models/PropertyPortfolioManager.Models.csproj index 12680f7..db63d90 100644 --- a/src/PropertyPortfolioManager.Models/PropertyPortfolioManager.Models.csproj +++ b/src/PropertyPortfolioManager.Models/PropertyPortfolioManager.Models.csproj @@ -8,6 +8,7 @@ + diff --git a/src/PropertyPortfolioManager.Server.Repositories.Interfaces/IBankStatementRepository.cs b/src/PropertyPortfolioManager.Server.Repositories.Interfaces/IBankStatementRepository.cs new file mode 100644 index 0000000..98a7580 --- /dev/null +++ b/src/PropertyPortfolioManager.Server.Repositories.Interfaces/IBankStatementRepository.cs @@ -0,0 +1,10 @@ +using PropertyPortfolioManager.Models.Model.Finance; +using System.Data; + +namespace PropertyPortfolioManager.Server.Repositories.Interfaces +{ + public interface IBankStatementRepository + { + Task AddBankStatementRecords(int currentUserId, int accountId, DataTable recordList); + } +} diff --git a/src/PropertyPortfolioManager.Server.Repositories/BankStatementRepository.cs b/src/PropertyPortfolioManager.Server.Repositories/BankStatementRepository.cs new file mode 100644 index 0000000..227dd70 --- /dev/null +++ b/src/PropertyPortfolioManager.Server.Repositories/BankStatementRepository.cs @@ -0,0 +1,29 @@ +using Dapper; +using Microsoft.Graph.Models; +using PropertyPortfolioManager.Models.Model.Finance; +using PropertyPortfolioManager.Server.Repositories.Interfaces; +using System.ComponentModel; +using System.Data; + +namespace PropertyPortfolioManager.Server.Repositories +{ + public class BankStatementRepository : IBankStatementRepository + { + private readonly IDbConnection dbConnection; + + public BankStatementRepository(IDbConnection dbConnection) + { + this.dbConnection = dbConnection; + } + + public async Task AddBankStatementRecords(int currentUserId, int accountId, DataTable recordList) + { + var parameters = new DynamicParameters(); + parameters.Add("@AccountId", accountId); + parameters.Add("@Statement", recordList.AsTableValuedParameter("[finance].[StatementTableType]")); + parameters.Add("@CurrentUserId", currentUserId); + + await this.dbConnection.ExecuteAsync("finance.BankStatement_Upload", parameters, commandType: CommandType.StoredProcedure); + } + } +} diff --git a/src/PropertyPortfolioManager.Server.Services.Interfaces/IBankStatementService.cs b/src/PropertyPortfolioManager.Server.Services.Interfaces/IBankStatementService.cs new file mode 100644 index 0000000..2301ed1 --- /dev/null +++ b/src/PropertyPortfolioManager.Server.Services.Interfaces/IBankStatementService.cs @@ -0,0 +1,9 @@ +using PropertyPortfolioManager.Models.Model.Document; + +namespace PropertyPortfolioManager.Server.Services.Interfaces +{ + public interface IBankStatementService + { + Task UploadBankStatement(int currentUserId, int portfolioId, Stream stream); + } +} diff --git a/src/PropertyPortfolioManager.Server.Services/BankStatementService.cs b/src/PropertyPortfolioManager.Server.Services/BankStatementService.cs new file mode 100644 index 0000000..30cc800 --- /dev/null +++ b/src/PropertyPortfolioManager.Server.Services/BankStatementService.cs @@ -0,0 +1,47 @@ +using CsvHelper; +using CsvHelper.Configuration; +using DRJTechnology.Cache; +using Microsoft.Extensions.Options; +using PropertyPortfolioManager.Models.Model.Finance; +using PropertyPortfolioManager.Server.Repositories.Interfaces; +using PropertyPortfolioManager.Server.Services.Helpers; +using PropertyPortfolioManager.Server.Services.Interfaces; +using PropertyPortfolioManager.Server.Shared.Configuration; +using System.Globalization; + +namespace PropertyPortfolioManager.Server.Services +{ + public class BankStatementService : IBankStatementService + { + private Settings settings; + private readonly ICacheService cacheService; + private readonly IBankStatementRepository bankStatementRepository; + + public BankStatementService(IOptions settings, ICacheService cacheService, IBankStatementRepository bankStatementRepository) + { + this.settings = settings.Value; + this.cacheService = cacheService; + this.bankStatementRepository = bankStatementRepository; + } + + public async Task UploadBankStatement(int currentUserId, int portfolioId, Stream stream) + { + var config = new CsvConfiguration(new CultureInfo("en-GB")); + config.MissingFieldFound = null; + config.IgnoreBlankLines = true; + + var recordList = new List(); + using (var reader = new StreamReader(stream)) + using (var csv = new CsvReader(reader, config)) + { + csv.Context.RegisterClassMap(); + var records = csv.GetRecords(); + recordList = records.ToList(); + } + var dataTable = DataHelpers.ConvertToDataTable(recordList); + + await this.bankStatementRepository.AddBankStatementRecords(currentUserId, portfolioId, dataTable); + + } + } +} diff --git a/src/PropertyPortfolioManager.Server.Services/DocumentService.cs b/src/PropertyPortfolioManager.Server.Services/DocumentService.cs index 1ccc470..81f540b 100644 --- a/src/PropertyPortfolioManager.Server.Services/DocumentService.cs +++ b/src/PropertyPortfolioManager.Server.Services/DocumentService.cs @@ -1,13 +1,18 @@ using AutoMapper; +using CsvHelper; +using CsvHelper.Configuration; using DRJTechnology.Cache; using Microsoft.Extensions.Options; using Microsoft.Graph; +using Microsoft.Graph.Models; using Microsoft.IdentityModel.Tokens; using PropertyPortfolioManager.Models.CacheKeys; using PropertyPortfolioManager.Models.Model.Document; +using PropertyPortfolioManager.Models.Model.Finance; using PropertyPortfolioManager.Models.Model.Property; using PropertyPortfolioManager.Server.Services.Interfaces; using PropertyPortfolioManager.Server.Shared.Configuration; +using System.Globalization; namespace PropertyPortfolioManager.Server.Services { diff --git a/src/PropertyPortfolioManager.Server.Services/Helpers/DataHelpers.cs b/src/PropertyPortfolioManager.Server.Services/Helpers/DataHelpers.cs new file mode 100644 index 0000000..eebb066 --- /dev/null +++ b/src/PropertyPortfolioManager.Server.Services/Helpers/DataHelpers.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; +using System.Data; + +namespace PropertyPortfolioManager.Server.Services.Helpers +{ + internal static class DataHelpers + { + public static DataTable ConvertToDataTable(List data) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T)); + DataTable table = new DataTable(); + foreach (PropertyDescriptor prop in properties) + { + table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); + } + foreach (T item in data) + { + DataRow row = table.NewRow(); + foreach (PropertyDescriptor prop in properties) + { + row[prop.Name] = prop.GetValue(item) ?? DBNull.Value; + } + table.Rows.Add(row); + } + return table; + } + } +} diff --git a/src/PropertyPortfolioManager.Server.Services/PropertyPortfolioManager.Server.Services.csproj b/src/PropertyPortfolioManager.Server.Services/PropertyPortfolioManager.Server.Services.csproj index e8268c9..11efe5f 100644 --- a/src/PropertyPortfolioManager.Server.Services/PropertyPortfolioManager.Server.Services.csproj +++ b/src/PropertyPortfolioManager.Server.Services/PropertyPortfolioManager.Server.Services.csproj @@ -8,6 +8,7 @@ + diff --git a/src/PropertyPortfolioManager.Server/Controllers/BankStatementController.cs b/src/PropertyPortfolioManager.Server/Controllers/BankStatementController.cs new file mode 100644 index 0000000..937c77a --- /dev/null +++ b/src/PropertyPortfolioManager.Server/Controllers/BankStatementController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web.Resource; +using PropertyPortfolioManager.Models.InternalObjects; +using PropertyPortfolioManager.Server.Services.Interfaces; + +namespace PropertyPortfolioManager.Server.Controllers +{ + [Authorize] + [Route("api/[controller]")] + [ApiController] + [RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")] + public class BankStatementController : BaseController + { + private readonly ILogger logger; + private readonly IDocumentService documentService; + private readonly IBankStatementService bankStatementService; + + public BankStatementController(ILogger logger, IUserService userService, IBankStatementService bankStatementService) + : base(userService) + { + this.logger = logger; + this.bankStatementService = bankStatementService; + } + + [HttpPost] + [Route("UploadBankStatement")] + public async Task UploadBankStatement() + { + try + { + var portfolioId = (await this.GetCurrentUser()).SelectedPortfolioId; + if (portfolioId == null) + { + return Ok( new PpmApiResponse() + { + Success = false, + ErrorMessage = "UploadBankStatemente: User has no Selected Portfolio Id set." + }); + } + else + { + var file = Request.Form.Files[0]; + if (file == null || file.Length == 0) + return BadRequest("Please select a file to upload."); + + var stream = file.OpenReadStream(); + await this.bankStatementService.UploadBankStatement((await this.GetCurrentUser()).Id, (int)portfolioId, stream); + return Ok(); + } + } + catch (Exception ex) + { + throw; + } + } + } +} \ No newline at end of file diff --git a/src/PropertyPortfolioManager.Server/Controllers/UploadController.cs b/src/PropertyPortfolioManager.Server/Controllers/UploadController.cs new file mode 100644 index 0000000..a7749d1 --- /dev/null +++ b/src/PropertyPortfolioManager.Server/Controllers/UploadController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Net.Http.Headers; + +namespace PropertyPortfolioManager.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class UploadController : ControllerBase + { + [HttpPost] + public IActionResult Upload() + { + try + { + var file = Request.Form.Files[0]; + var folderName = Path.Combine("StaticFiles", "Images"); + var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName); + if (file.Length > 0) + { + var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); + var fullPath = Path.Combine(pathToSave, fileName); + var dbPath = Path.Combine(folderName, fileName); + using (var stream = new FileStream(fullPath, FileMode.Create)) + { + file.CopyTo(stream); + } + return Ok(dbPath); + } + else + { + return BadRequest(); + } + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex}"); + } + } + } +} diff --git a/src/PropertyPortfolioManager.Server/ServiceCollectionExtension.cs b/src/PropertyPortfolioManager.Server/ServiceCollectionExtension.cs index 2968188..acac6d4 100644 --- a/src/PropertyPortfolioManager.Server/ServiceCollectionExtension.cs +++ b/src/PropertyPortfolioManager.Server/ServiceCollectionExtension.cs @@ -48,6 +48,7 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -60,6 +61,7 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddTransient(db => new SqlConnection(configuration.GetConnectionString("PpmDatabaseConnectionString")));