diff --git a/StockWebHistory.sln b/StockWebHistory.sln index 3b6dd24..2f3a7e0 100644 --- a/StockWebHistory.sln +++ b/StockWebHistory.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.37614.0 d17.14 +VisualStudioVersion = 17.14.37614.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StockWebHistory", "StockWebHistory\StockWebHistory.csproj", "{76B5FDA3-FD94-4E06-BB2C-8CDDE8C205DD}" EndProject diff --git a/StockWebHistory/Controllers/DataLoadController.cs b/StockWebHistory/Controllers/DataLoadController.cs new file mode 100644 index 0000000..fb92d03 --- /dev/null +++ b/StockWebHistory/Controllers/DataLoadController.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Mvc; +using StockWebHistory.Services; + +namespace StockWebHistory.Controllers +{ + public class DataLoadController : Controller + { + private readonly IFileProcessingService _processingService; + + public DataLoadController(IFileProcessingService processingService) + { + _processingService = processingService; + } + + + public IActionResult Index() + { + return View(); + } + + public IActionResult Files() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task UploadAndProcess(IFormFile file) + { + // 1. Grundläggande validering + if (file == null || file.Length == 0) + { + TempData["Error"] = "Vänligen välj en giltig fil."; + return RedirectToAction(nameof(Index)); + } + + // Validera filtillägg om det behövs (t.ex. endast .csv) + var allowedExtensions = new[] { ".csv", ".json", ".xlsx" }; + var extension = System.IO.Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!allowedExtensions.Contains(extension)) + { + TempData["Error"] = "Ogiltigt filformat."; + return RedirectToAction(nameof(Index)); + } + + try + { + // 2. Skicka filströmmen direkt till processingservicen + using (var stream = file.OpenReadStream()) + { + int rowsProcessed = await _processingService.ProcessFileAsync(stream, extension); + TempData["Success"] = $"Klart! {rowsProcessed} rader har sparats i databasen."; + } + } + catch (System.Exception ex) + { + // Logga felet i din loggleda + TempData["Error"] = $"Ett fel uppstod vid bearbetningen: {ex.Message}"; + } + + return RedirectToAction(nameof(Index)); + } + + } +} diff --git a/StockWebHistory/Data/StockWebDbContext.cs b/StockWebHistory/Data/StockWebDbContext.cs index 8bb179e..48ca7a3 100644 --- a/StockWebHistory/Data/StockWebDbContext.cs +++ b/StockWebHistory/Data/StockWebDbContext.cs @@ -8,6 +8,24 @@ namespace StockWebHistory.Data { } public DbSet TransactionNotes { get; set; } - public DbSet StockEquities { get; set; } + public DbSet Stocks { get; set; } + + + override protected void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .HasIndex(t => new { t.StockCode, t.TransactionDate }) + .IsUnique(true); + modelBuilder.Entity() + .HasIndex(s => new { s.Bought }) + .IsUnique(false); + modelBuilder.Entity() + .HasIndex(s => new { s.Sold }) + .IsUnique(false); + modelBuilder.Entity() + .HasIndex(s => new { s.Bought, s.StockCode, s.Quantity }) + .IsUnique(false); + } } -} +} \ No newline at end of file diff --git a/StockWebHistory/Migrations/20260909152107_initialcreate.Designer.cs b/StockWebHistory/Migrations/20260909152107_initialcreate.Designer.cs new file mode 100644 index 0000000..a8c0160 --- /dev/null +++ b/StockWebHistory/Migrations/20260909152107_initialcreate.Designer.cs @@ -0,0 +1,109 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StockWebHistory.Data; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + [DbContext(typeof(StockWebDbContext))] + [Migration("20260909152107_initialcreate")] + partial class initialcreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("StockWebHistory.Models.StockEquity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bought") + .HasColumnType("TEXT"); + + b.Property("BoughtPrice") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Sold") + .HasColumnType("TEXT"); + + b.Property("SoldPrice") + .HasColumnType("TEXT"); + + b.Property("SoldQuant") + .HasColumnType("INTEGER"); + + b.Property("StockCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Bought"); + + b.HasIndex("Sold"); + + b.HasIndex("Bought", "StockCode", "Quantity"); + + b.ToTable("StockEquities"); + }); + + modelBuilder.Entity("StockWebHistory.Models.TransactionNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountToPay") + .HasColumnType("TEXT"); + + b.Property("Commission") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExchangeRate") + .HasColumnType("TEXT"); + + b.Property("StockCode") + .HasColumnType("TEXT"); + + b.Property("StockPrice") + .HasColumnType("TEXT"); + + b.Property("StockQuantity") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.Property("TransactionType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StockCode", "TransactionDate") + .IsUnique(); + + b.ToTable("TransactionNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StockWebHistory/Migrations/20260909152107_initialcreate.cs b/StockWebHistory/Migrations/20260909152107_initialcreate.cs new file mode 100644 index 0000000..6811444 --- /dev/null +++ b/StockWebHistory/Migrations/20260909152107_initialcreate.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + /// + public partial class initialcreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StockEquities", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + StockCode = table.Column(type: "TEXT", nullable: false), + Bought = table.Column(type: "TEXT", nullable: false), + BoughtPrice = table.Column(type: "TEXT", nullable: false), + Quantity = table.Column(type: "INTEGER", nullable: false), + Sold = table.Column(type: "TEXT", nullable: false), + SoldPrice = table.Column(type: "TEXT", nullable: false), + SoldQuant = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StockEquities", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TransactionNotes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TransactionDate = table.Column(type: "TEXT", nullable: false), + TransactionType = table.Column(type: "TEXT", nullable: true), + StockCode = table.Column(type: "TEXT", nullable: true), + StockPrice = table.Column(type: "TEXT", nullable: false), + ExchangeRate = table.Column(type: "TEXT", nullable: false), + Currency = table.Column(type: "TEXT", nullable: false), + StockQuantity = table.Column(type: "INTEGER", nullable: false), + TotalAmount = table.Column(type: "TEXT", nullable: false), + Commission = table.Column(type: "TEXT", nullable: false), + AmountToPay = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TransactionNotes", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_StockEquities_Bought", + table: "StockEquities", + column: "Bought"); + + migrationBuilder.CreateIndex( + name: "IX_StockEquities_Bought_StockCode_Quantity", + table: "StockEquities", + columns: new[] { "Bought", "StockCode", "Quantity" }); + + migrationBuilder.CreateIndex( + name: "IX_StockEquities_Sold", + table: "StockEquities", + column: "Sold"); + + migrationBuilder.CreateIndex( + name: "IX_TransactionNotes_StockCode_TransactionDate", + table: "TransactionNotes", + columns: new[] { "StockCode", "TransactionDate" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StockEquities"); + + migrationBuilder.DropTable( + name: "TransactionNotes"); + } + } +} diff --git a/StockWebHistory/Migrations/20260911143047_ReconcilingMigration.Designer.cs b/StockWebHistory/Migrations/20260911143047_ReconcilingMigration.Designer.cs new file mode 100644 index 0000000..f125361 --- /dev/null +++ b/StockWebHistory/Migrations/20260911143047_ReconcilingMigration.Designer.cs @@ -0,0 +1,109 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StockWebHistory.Data; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + [DbContext(typeof(StockWebDbContext))] + [Migration("20260911143047_ReconcilingMigration")] + partial class ReconcilingMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("StockWebHistory.Models.StockEquity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bought") + .HasColumnType("TEXT"); + + b.Property("BoughtPrice") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Sold") + .HasColumnType("TEXT"); + + b.Property("SoldPrice") + .HasColumnType("TEXT"); + + b.Property("SoldQuant") + .HasColumnType("INTEGER"); + + b.Property("StockCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Bought"); + + b.HasIndex("Sold"); + + b.HasIndex("Bought", "StockCode", "Quantity"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("StockWebHistory.Models.TransactionNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountToPay") + .HasColumnType("TEXT"); + + b.Property("Commission") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExchangeRate") + .HasColumnType("TEXT"); + + b.Property("StockCode") + .HasColumnType("TEXT"); + + b.Property("StockPrice") + .HasColumnType("TEXT"); + + b.Property("StockQuantity") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.Property("TransactionType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StockCode", "TransactionDate") + .IsUnique(); + + b.ToTable("TransactionNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StockWebHistory/Migrations/20260911143047_ReconcilingMigration.cs b/StockWebHistory/Migrations/20260911143047_ReconcilingMigration.cs new file mode 100644 index 0000000..2b26c6b --- /dev/null +++ b/StockWebHistory/Migrations/20260911143047_ReconcilingMigration.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + /// + public partial class ReconcilingMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_StockEquities", + table: "StockEquities"); + + migrationBuilder.RenameTable( + name: "StockEquities", + newName: "Stocks"); + + migrationBuilder.RenameIndex( + name: "IX_StockEquities_Sold", + table: "Stocks", + newName: "IX_Stocks_Sold"); + + migrationBuilder.RenameIndex( + name: "IX_StockEquities_Bought_StockCode_Quantity", + table: "Stocks", + newName: "IX_Stocks_Bought_StockCode_Quantity"); + + migrationBuilder.RenameIndex( + name: "IX_StockEquities_Bought", + table: "Stocks", + newName: "IX_Stocks_Bought"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Stocks", + table: "Stocks", + column: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_Stocks", + table: "Stocks"); + + migrationBuilder.RenameTable( + name: "Stocks", + newName: "StockEquities"); + + migrationBuilder.RenameIndex( + name: "IX_Stocks_Sold", + table: "StockEquities", + newName: "IX_StockEquities_Sold"); + + migrationBuilder.RenameIndex( + name: "IX_Stocks_Bought_StockCode_Quantity", + table: "StockEquities", + newName: "IX_StockEquities_Bought_StockCode_Quantity"); + + migrationBuilder.RenameIndex( + name: "IX_Stocks_Bought", + table: "StockEquities", + newName: "IX_StockEquities_Bought"); + + migrationBuilder.AddPrimaryKey( + name: "PK_StockEquities", + table: "StockEquities", + column: "Id"); + } + } +} diff --git a/StockWebHistory/Migrations/20260912145310_decimal_can_be_nullable.Designer.cs b/StockWebHistory/Migrations/20260912145310_decimal_can_be_nullable.Designer.cs new file mode 100644 index 0000000..31fe891 --- /dev/null +++ b/StockWebHistory/Migrations/20260912145310_decimal_can_be_nullable.Designer.cs @@ -0,0 +1,109 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StockWebHistory.Data; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + [DbContext(typeof(StockWebDbContext))] + [Migration("20260912145310_decimal_can_be_nullable")] + partial class decimal_can_be_nullable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("StockWebHistory.Models.StockEquity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bought") + .HasColumnType("TEXT"); + + b.Property("BoughtPrice") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Sold") + .HasColumnType("TEXT"); + + b.Property("SoldPrice") + .HasColumnType("TEXT"); + + b.Property("SoldQuant") + .HasColumnType("INTEGER"); + + b.Property("StockCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Bought"); + + b.HasIndex("Sold"); + + b.HasIndex("Bought", "StockCode", "Quantity"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("StockWebHistory.Models.TransactionNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountToPay") + .HasColumnType("TEXT"); + + b.Property("Commission") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExchangeRate") + .HasColumnType("TEXT"); + + b.Property("StockCode") + .HasColumnType("TEXT"); + + b.Property("StockPrice") + .HasColumnType("TEXT"); + + b.Property("StockQuantity") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.Property("TransactionType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StockCode", "TransactionDate") + .IsUnique(); + + b.ToTable("TransactionNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StockWebHistory/Migrations/20260912145310_decimal_can_be_nullable.cs b/StockWebHistory/Migrations/20260912145310_decimal_can_be_nullable.cs new file mode 100644 index 0000000..a5f9860 --- /dev/null +++ b/StockWebHistory/Migrations/20260912145310_decimal_can_be_nullable.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + /// + public partial class decimal_can_be_nullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TotalAmount", + table: "TransactionNotes", + type: "TEXT", + nullable: true, + oldClrType: typeof(decimal), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "Commission", + table: "TransactionNotes", + type: "TEXT", + nullable: true, + oldClrType: typeof(decimal), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "AmountToPay", + table: "TransactionNotes", + type: "TEXT", + nullable: true, + oldClrType: typeof(decimal), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TotalAmount", + table: "TransactionNotes", + type: "TEXT", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Commission", + table: "TransactionNotes", + type: "TEXT", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AmountToPay", + table: "TransactionNotes", + type: "TEXT", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "TEXT", + oldNullable: true); + } + } +} diff --git a/StockWebHistory/Migrations/20260913143435_decimal_can_be_nullable_more.Designer.cs b/StockWebHistory/Migrations/20260913143435_decimal_can_be_nullable_more.Designer.cs new file mode 100644 index 0000000..6d93dc6 --- /dev/null +++ b/StockWebHistory/Migrations/20260913143435_decimal_can_be_nullable_more.Designer.cs @@ -0,0 +1,109 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StockWebHistory.Data; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + [DbContext(typeof(StockWebDbContext))] + [Migration("20260913143435_decimal_can_be_nullable_more")] + partial class decimal_can_be_nullable_more + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("StockWebHistory.Models.StockEquity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bought") + .HasColumnType("TEXT"); + + b.Property("BoughtPrice") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Sold") + .HasColumnType("TEXT"); + + b.Property("SoldPrice") + .HasColumnType("TEXT"); + + b.Property("SoldQuant") + .HasColumnType("INTEGER"); + + b.Property("StockCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Bought"); + + b.HasIndex("Sold"); + + b.HasIndex("Bought", "StockCode", "Quantity"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("StockWebHistory.Models.TransactionNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountToPay") + .HasColumnType("TEXT"); + + b.Property("Commission") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExchangeRate") + .HasColumnType("TEXT"); + + b.Property("StockCode") + .HasColumnType("TEXT"); + + b.Property("StockPrice") + .HasColumnType("TEXT"); + + b.Property("StockQuantity") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.Property("TransactionType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StockCode", "TransactionDate") + .IsUnique(); + + b.ToTable("TransactionNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StockWebHistory/Migrations/20260913143435_decimal_can_be_nullable_more.cs b/StockWebHistory/Migrations/20260913143435_decimal_can_be_nullable_more.cs new file mode 100644 index 0000000..2f865ac --- /dev/null +++ b/StockWebHistory/Migrations/20260913143435_decimal_can_be_nullable_more.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + /// + public partial class decimal_can_be_nullable_more : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExchangeRate", + table: "TransactionNotes", + type: "TEXT", + nullable: true, + oldClrType: typeof(decimal), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExchangeRate", + table: "TransactionNotes", + type: "TEXT", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "TEXT", + oldNullable: true); + } + } +} diff --git a/StockWebHistory/Migrations/StockWebDbContextModelSnapshot.cs b/StockWebHistory/Migrations/StockWebDbContextModelSnapshot.cs new file mode 100644 index 0000000..6ed848a --- /dev/null +++ b/StockWebHistory/Migrations/StockWebDbContextModelSnapshot.cs @@ -0,0 +1,106 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StockWebHistory.Data; + +#nullable disable + +namespace StockWebHistory.Migrations +{ + [DbContext(typeof(StockWebDbContext))] + partial class StockWebDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("StockWebHistory.Models.StockEquity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bought") + .HasColumnType("TEXT"); + + b.Property("BoughtPrice") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Sold") + .HasColumnType("TEXT"); + + b.Property("SoldPrice") + .HasColumnType("TEXT"); + + b.Property("SoldQuant") + .HasColumnType("INTEGER"); + + b.Property("StockCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Bought"); + + b.HasIndex("Sold"); + + b.HasIndex("Bought", "StockCode", "Quantity"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("StockWebHistory.Models.TransactionNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountToPay") + .HasColumnType("TEXT"); + + b.Property("Commission") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExchangeRate") + .HasColumnType("TEXT"); + + b.Property("StockCode") + .HasColumnType("TEXT"); + + b.Property("StockPrice") + .HasColumnType("TEXT"); + + b.Property("StockQuantity") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.Property("TransactionType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StockCode", "TransactionDate") + .IsUnique(); + + b.ToTable("TransactionNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StockWebHistory/Models/FileUploadViewModel.cs b/StockWebHistory/Models/FileUploadViewModel.cs new file mode 100644 index 0000000..2dbf42f --- /dev/null +++ b/StockWebHistory/Models/FileUploadViewModel.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace StockWebHistory.Models; + +public class FileUploadViewModel +{ + [Required(ErrorMessage = "Vänligen välj en fil")] + [Display(Name = "Välj fil")] + public IFormFile File { get; set; } + + // Du kan även lägga till fler fält om du vill skicka med mer data i formuläret + public string Description { get; set; } +} diff --git a/StockWebHistory/Models/TransactionNote.cs b/StockWebHistory/Models/TransactionNote.cs index ab30420..7930b8d 100644 --- a/StockWebHistory/Models/TransactionNote.cs +++ b/StockWebHistory/Models/TransactionNote.cs @@ -7,12 +7,12 @@ public class TransactionNote public string? TransactionType { get; set; } public string? StockCode { get; set; } public decimal StockPrice { get; set; } - public decimal ExchangeRate { get; set; } + public decimal? ExchangeRate { get; set; } public string Currency { get; set; } public int StockQuantity { get; set; } - public decimal TotalAmount { get; set; } - public decimal Commission { get; set; } - public decimal AmountToPay { get; set; } + public decimal? TotalAmount { get; set; } + public decimal? Commission { get; set; } + public decimal? AmountToPay { get; set; } public override string ToString() { diff --git a/StockWebHistory/Models/TransactionNoteMap.cs b/StockWebHistory/Models/TransactionNoteMap.cs new file mode 100644 index 0000000..4a73729 --- /dev/null +++ b/StockWebHistory/Models/TransactionNoteMap.cs @@ -0,0 +1,32 @@ +using CsvHelper.Configuration; + +namespace StockWebHistory.Models +{ + public class TransactionNoteMap : ClassMap + { + public TransactionNoteMap() + { + + + // Ignorera Id eftersom databasen automatiskt genererar det (Auto-increment) + Map(m => m.Id).Ignore(); + // Matcha CSV-kolumner mot C#-egenskaper + Map(m => m.StockCode).Name(@"Värdepapper/beskrivning", @"Värdepapper"); // Exemplet kollar flera alternativa namn + Map(m => m.TransactionDate).Name("Datum", "Date", "Affärsdag"); + Map(m => m.TransactionType).Name("Typ av transaktion", "Transaktion"); + Map(m => m.StockQuantity).Name("Antal", "Innehavsförändring"); + Map(m => m.StockPrice).Name("Kurs"); + Map(m => m.TotalAmount).Name("Belopp", "Likvidbelopp (Betalvaluta)"); + Map(m => m.Currency).Name("Instrumentvaluta", @"Noteringsvaluta"); + Map(m => m.Commission).Name("Courtage") + .TypeConverterOption + .NullValues(string.Empty, " ", "NULL", "null"); + Map(m => m.ExchangeRate).Name("Valutakurs", "Växlingskurs") + .TypeConverterOption + .NullValues(string.Empty, " ", "NULL", "null"); + Map(m => m.AmountToPay).Name("Belopp", "Likvidbelopp (Betalvaluta)") + .TypeConverterOption + .NullValues(string.Empty, " ", "NULL", "null"); + } + } +} diff --git a/StockWebHistory/Program.cs b/StockWebHistory/Program.cs index 13262fd..69ae2b1 100644 --- a/StockWebHistory/Program.cs +++ b/StockWebHistory/Program.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using StockWebHistory.Data; +using StockWebHistory.Services; namespace StockWebHistory { @@ -12,12 +14,24 @@ namespace StockWebHistory // Add services to the container.x builder.Services.AddControllersWithViews(); + builder.Services.AddDbContext(options => - options.UseInMemoryDatabase("StockWebHistoryDB") + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")) + .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)) ); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + var app = builder.Build(); + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + } + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { diff --git a/StockWebHistory/Services/FileProcessingService.cs b/StockWebHistory/Services/FileProcessingService.cs new file mode 100644 index 0000000..3e97cce --- /dev/null +++ b/StockWebHistory/Services/FileProcessingService.cs @@ -0,0 +1,145 @@ +using CsvHelper; +using CsvHelper.Configuration; +using Microsoft.Data.Sqlite; +using StockWebHistory.Data; +using StockWebHistory.Models; +using System.Globalization; + +namespace StockWebHistory.Services; + +public class FileProcessingService : IFileProcessingService +{ + private readonly StockWebDbContext _db; + private readonly ILogger _logger; + private readonly ITransactionNotesServices _notesServices; + + public FileProcessingService(StockWebDbContext db, + ILogger logger, + ITransactionNotesServices notesServices) + { + _db = db; + _logger = logger; + _notesServices = notesServices; + } + + public async Task ProcessFileAsync(Stream stream, string fileExtension) + { + var recordsToInsert = new List(); + bool retry = true; + // Exempel för CSV-läsning med StreamReader + if (fileExtension == ".csv") + { + + var config = new CsvConfiguration(new CultureInfo("sv-SE")) // Hanterar t.ex. komma som decimalskiljetecken + { + HasHeaderRecord = true, + TrimOptions = TrimOptions.Trim, + MissingFieldFound = null, // Ignorera om något fält saknas istället för att krascha + HeaderValidated = null, // Ignorera om någon header inte matchar exakt + Delimiter = ";" // Ange avgränsare, t.ex. komma eller semikolon + }; + + // Om din CSV-fil använder KOMMA (,) som decimaltecken: + var configKomma = new CsvConfiguration(new CultureInfo("sv-SE")); + + using (var reader = new StreamReader(stream)) + { + // Om din CSV-fil använder semikolon (;) istället för komma (,) + // kan du automatiskt upptäcka eller sätta avdelaren så här: + /* + using (var csvReader = new CsvReader(reader, config)) + { + csvReader.Context.RegisterClassMap(); + } + */ + + using (var csv = new CsvReader(reader, config)) + { + // Registrera din ClassMap om kolumnnamnen skiljer sig från C#-modellen + csv.Context.RegisterClassMap(); + + // 2. Läsa in posterna asynkront + var records = new List(); + + await foreach (var record in csv.GetRecordsAsync()) + { + records.Add(record); + } + + if (records.Count == 0) + { + return 0; + } + + // 3. Spara alla poster till databasen i en batch med EF Core + foreach (var record in records) + { + //recordsToInsert.Add(record); + while (retry) + { + try + { + await _db.TransactionNotes.AddAsync(record); + await _db.SaveChangesAsync(); + retry = false; // Om det lyckas, sätt retry till false + } + catch (Exception ex) when (ex.InnerException is SqliteException sqlite && sqlite.SqliteErrorCode == 19) + { + _logger.LogError("Fel vid sparande till databas (unik nyckel): " + ex.Message + "{" + record.ToString() + "}"); + if (await check_and_correct_table_unity(record)) + retry = true; // Om tabellen korrigerades, försök igen + else + retry = false; + } + catch (Exception ex) + { + _logger.LogError("Fel vid sparande till databas: " + ex.Message); + retry = false; + } + } + retry = true; // Återställ retry för nästa record + } + return records.Count; + } + + } + } + + // Spara alla rader i databasen i en batch + if (recordsToInsert.Count > 0) + { + await _db.TransactionNotes.AddRangeAsync(recordsToInsert); + await _db.SaveChangesAsync(); + } + + return recordsToInsert.Count; + } + + private async Task check_and_correct_table_unity(TransactionNote localTrans) + { + var result = false; + if (string.IsNullOrEmpty(localTrans.StockCode)) + { + return false; + } + var jmfPost = await _notesServices.GetByIndexAsync(localTrans.StockCode, localTrans.TransactionDate); + if (jmfPost != null) + { + if (jmfPost.TransactionType == localTrans.TransactionType + && jmfPost.StockPrice == localTrans.StockPrice + && jmfPost.StockQuantity == localTrans.StockQuantity + && jmfPost.AmountToPay == localTrans.AmountToPay + ) + { + result = false; + } + else + { + DateTime transDatTemp = localTrans.TransactionDate.AddSeconds(5); + localTrans.TransactionDate = transDatTemp; + result = true; + } + } + return result; + } +} diff --git a/StockWebHistory/Services/HandleCsvNotes.cs b/StockWebHistory/Services/HandleCsvNotes.cs new file mode 100644 index 0000000..a1990cc --- /dev/null +++ b/StockWebHistory/Services/HandleCsvNotes.cs @@ -0,0 +1,134 @@ +using StockWebHistory.Models; +using System.Globalization; + +namespace StockWebHistory; + +public class HandleCsvNotes : IHandleCsvNotes +{ + + private List lines = new(); + private string[] Header_table = new string[0]; + + public List GetTransactionNotes(string filePath) + { + lines = File.ReadAllLines(filePath).ToList(); + Header_table = lines[0].Split(';'); + var transactionNotes = new List(); + foreach (var line in lines.Skip(1)) + { + var values = line.Split(';'); + var test = values[NamePos(Header_table, "Typ av transaktion")]; + if (test == "Köp" || test == "Sälj") + { + try + { + var transactionNote = new TransactionNote + { + TransactionDate = DateTime.Parse(values[NamePos(Header_table, "Datum")]), + TransactionType = values[NamePos(Header_table, "Typ av transaktion")] == "Köp" ? "KÖP" : "SÄLJ", + StockCode = values[NamePos(Header_table, @"Värdepapper/beskrivning")], + StockPrice = decimal.Parse((values[NamePos(Header_table, "Kurs")] == "") ? "0" : values[NamePos(Header_table, "Kurs")]), + Currency = values[NamePos(Header_table, @"Instrumentvaluta")], + StockQuantity = Math.Abs(ParseCsvNumber(values[NamePos(Header_table, "Antal")])), + ExchangeRate = decimal.Parse((values[NamePos(Header_table, "Valutakurs")] == "") ? "0" : "0" + values[NamePos(Header_table, "Valutakurs")]), + Commission = decimal.Parse((values[NamePos(Header_table, "Courtage")] == "") ? "0" : "0" + values[NamePos(Header_table, "Courtage")]), + TotalAmount = Math.Abs(decimal.Parse((values[NamePos(Header_table, "Belopp")] == "") ? "0" : values[NamePos(Header_table, "Belopp")])) - decimal.Parse((values[NamePos(Header_table, "Courtage")] == "") ? "0" : values[NamePos(Header_table, "Courtage")]), + AmountToPay = Math.Abs(decimal.Parse((values[NamePos(Header_table, "Belopp")] == "") ? "0" : values[NamePos(Header_table, "Belopp")])) + }; + transactionNotes.Add(transactionNote); + } + catch (Exception ex) + { + MessageBox.Show($"Warning: '{ex.Message}'"); + } + } + } + return transactionNotes; + } + + public List GetSHBTransactionNotes(string filePath) + { + lines = File.ReadAllLines(filePath).ToList(); + Header_table = lines[0].Split(';'); + var transactionNotes = new List(); + foreach (var line in lines.Skip(1)) + { + var values = line.Split(';'); + var test = values[NamePos(Header_table, "Transaktion")]; + if (test == "Köp" || test == "Sälj") + { + try + { + var transactionNote = new TransactionNote + { + TransactionDate = DateTime.Parse(values[NamePos(Header_table, "Affärsdag")]), + TransactionType = values[NamePos(Header_table, "Transaktion")] == "Köp" ? "KÖP" : "SÄLJ", + StockCode = values[NamePos(Header_table, @"Värdepapper")], + StockPrice = decimal.Parse((values[NamePos(Header_table, "Kurs")] == "") ? "0" : values[NamePos(Header_table, "Kurs")]), + Currency = values[NamePos(Header_table, @"Noteringsvaluta")], + StockQuantity = Math.Abs(ParseCsvNumber(values[NamePos(Header_table, "Innehavsförändring")])), + ExchangeRate = decimal.Parse((values[NamePos(Header_table, "Växlingskurs")] == "") ? "0" : "0" + values[NamePos(Header_table, "Växlingskurs")]), + Commission = decimal.Parse((values[NamePos(Header_table, "Courtage")] == "") ? "0" : "0" + values[NamePos(Header_table, "Courtage")]), + TotalAmount = Math.Abs(decimal.Parse((values[NamePos(Header_table, "Likvidbelopp (Betalvaluta)")] == "") ? "0" : values[NamePos(Header_table, "Likvidbelopp (Betalvaluta)")])) - decimal.Parse((values[NamePos(Header_table, "Courtage")] == "") ? "0" : values[NamePos(Header_table, "Courtage")]), + AmountToPay = Math.Abs(decimal.Parse((values[NamePos(Header_table, "Likvidbelopp (Betalvaluta)")] == "") ? "0" : values[NamePos(Header_table, "Likvidbelopp (Betalvaluta)")])) + }; + transactionNotes.Add(transactionNote); + } + catch (Exception ex) + { + MessageBox.Show($"Warning: '{ex.Message}'"); + // throw; + } + } + } + return transactionNotes; + } + + + private int NamePos(string[] h_table, string value) + { + var pos = Array.IndexOf(h_table, value); + if (pos != -1) + { + return pos; + } + else + { + MessageBox.Show($"Warning: Unable to find '{value}'"); + return 0; + } + } + + public static int ParseCsvNumber(string input) + { + if (string.IsNullOrWhiteSpace(input)) + throw new ArgumentException("Input is empty"); + + // Försök först som int (snabbast) + if (int.TryParse(input, out int intValue)) + return intValue; + + // Försök som decimal (t.ex. "123.45" eller "123,45") + if (decimal.TryParse( + input, + NumberStyles.Any, + CultureInfo.InvariantCulture, + out decimal decValue)) + { + return (int)Math.Round(decValue, MidpointRounding.AwayFromZero); + } + + // Försök med svensk kultur (CSV från Excel brukar använda komma) + if (decimal.TryParse( + input, + NumberStyles.Any, + new CultureInfo("sv-SE"), + out decValue)) + { + return (int)Math.Round(decValue, MidpointRounding.AwayFromZero); + } + + throw new FormatException($"Kan inte tolka talet: '{input}'"); + } + +} \ No newline at end of file diff --git a/StockWebHistory/Services/IFileProcessingService.cs b/StockWebHistory/Services/IFileProcessingService.cs new file mode 100644 index 0000000..ed623a9 --- /dev/null +++ b/StockWebHistory/Services/IFileProcessingService.cs @@ -0,0 +1,7 @@ +namespace StockWebHistory.Services +{ + public interface IFileProcessingService + { + Task ProcessFileAsync(Stream stream, string fileExtension); + } +} diff --git a/StockWebHistory/Services/IHandleCsvNotes.cs b/StockWebHistory/Services/IHandleCsvNotes.cs new file mode 100644 index 0000000..a8ac55a --- /dev/null +++ b/StockWebHistory/Services/IHandleCsvNotes.cs @@ -0,0 +1,10 @@ +using StockWebHistory.Models; + +namespace StockWebHistory +{ + public interface IHandleCsvNotes + { + List GetTransactionNotes(string filePath); + List GetSHBTransactionNotes(string filePath); + } +} \ No newline at end of file diff --git a/StockWebHistory/Services/IStockEquityServices.cs b/StockWebHistory/Services/IStockEquityServices.cs new file mode 100644 index 0000000..094477c --- /dev/null +++ b/StockWebHistory/Services/IStockEquityServices.cs @@ -0,0 +1,13 @@ +using StockWebHistory.Models; + +namespace StockWebHistory.Services +{ + public interface IStockEquityServices + { + Task> GenerateStockVision(); + Task> GetAllAsync(); + Task> GetLatestSoldInfoAsync(); + Task> GetAllAsync(DateTime equityDate); + Task> GetByCodeAsync(string stockCode); + } +} \ No newline at end of file diff --git a/StockWebHistory/Services/ITransactionNotesServices.cs b/StockWebHistory/Services/ITransactionNotesServices.cs new file mode 100644 index 0000000..196843a --- /dev/null +++ b/StockWebHistory/Services/ITransactionNotesServices.cs @@ -0,0 +1,16 @@ +using StockWebHistory.Models; + +namespace StockWebHistory.Services +{ + public interface ITransactionNotesServices + { + Task AddAsync(TransactionNote transactionNote); + Task DeleteAsync(int id); + Task> GetAllExistingStockCodes(); + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task GetByIndexAsync(string stockCode, DateTime transDate); + Task> GetByStockCodeAsync(string stockCode); + Task UpdateAsync(TransactionNote transactionNote); + } +} \ No newline at end of file diff --git a/StockWebHistory/Services/StockEquityServices.cs b/StockWebHistory/Services/StockEquityServices.cs new file mode 100644 index 0000000..619641f --- /dev/null +++ b/StockWebHistory/Services/StockEquityServices.cs @@ -0,0 +1,229 @@ +using Microsoft.EntityFrameworkCore; +using StockWebHistory.Data; +using StockWebHistory.Models; +using System.Diagnostics; + +namespace StockWebHistory.Services; + +public class StockEquityServices : IStockEquityServices +{ + private readonly StockWebDbContext _dbContext; + private readonly ITransactionNotesServices _notesServices; + + public StockEquityServices(StockWebDbContext dbContext, ITransactionNotesServices notesServices) + { + _dbContext = dbContext; + _notesServices = notesServices; + } + + public async Task> GetAllAsync() + { + return await _dbContext.Stocks.ToListAsync(); + } + public async Task> GetAllAsync(DateTime equityDate) + { + return await _dbContext.Stocks + .Where(s => s.Bought <= equityDate && (s.Sold > equityDate || s.SoldQuant == 0)) + .ToListAsync(); + } + + public async Task> GetByCodeAsync(string stockCode) + { + var searchedNotes = await _notesServices.GetByStockCodeAsync(stockCode); + + return await _dbContext.Stocks + .Where(s => s.StockCode == stockCode) + .OrderBy(s => s.Bought) + .ToListAsync(); + } + + public async Task> GetLatestSoldInfoAsync() + { + var today = DateTime.Today; + + var maxPerCode = _dbContext.Stocks + .Where(s => s.Sold < today) + .GroupBy(s => s.StockCode) + .Select(g => new { StockCode = g.Key, MaxSold = g.Max(x => x.Sold) }); + + var minBoughtPerCodeAndSold = _dbContext.Stocks + .Where(s => s.Sold < today) + .Join(maxPerCode, + s => new { s.StockCode, s.Sold }, + m => new { m.StockCode, Sold = m.MaxSold }, + (s, m) => new { s.StockCode, s.Sold, s.BoughtPrice }) + .GroupBy(x => new { x.StockCode, x.Sold }) + .Select(g => new { g.Key.StockCode, g.Key.Sold, MinBought = g.Min(x => (double)x.BoughtPrice) }); + + var result = from s in _dbContext.Stocks + where s.Sold < today + join mm in minBoughtPerCodeAndSold + on new { s.StockCode, s.Sold, BoughtPrice = (double)s.BoughtPrice } + equals new { mm.StockCode, Sold = mm.Sold, BoughtPrice = mm.MinBought } + select s; + + return await result.ToListAsync(); + + //var query = + // from s in _dbContext.Stocks + // where s.Sold < today + // group s by s.StockCode into g + // let maxSold = g.Max(x => x.Sold) + // let minBought = g.Where(x => x.Sold == maxSold).Min(x => x.BoughtPrice) + // from row in g + // where row.Sold == maxSold && row.BoughtPrice == minBought + // select row; + + //return await query.ToListAsync(); + + } + + /// + /// Skapar en ny lista med aktieinnehav baserat på transaktionsnoteringar. + /// Den rensar först den befintliga listan och bygger sedan upp den igen genom att + /// utgående från alla befintliga notor skapa en lista med aktieinnehav. + /// + /// + public async Task> GenerateStockVision() + { + var oldStock = "OldStock"; + var nyAktie = false; + List equityWarningList = new List(); + + await _dbContext.Stocks.ExecuteDeleteAsync(); + await _dbContext.SaveChangesAsync(); + + var existingNotes = await _notesServices.GetAllAsync(); + + foreach (var note in existingNotes + .OrderBy(e => e.StockCode) + .ThenBy(e => e.TransactionDate) + //.OrderBy(e => e.TransactionType) + .ToList()) + { + // Kontrollera om TransactionType är null och StockCode inte är null + // (om det är fallet, hoppa över denna iteration) + Debug.WriteLine(note.ToString()); + + nyAktie = false; + if (note.StockCode != null && + note.StockCode != oldStock) + { + oldStock = note.StockCode; + nyAktie = true; + } + + + if (note.TransactionType == null && note.StockCode != null) { } + else + { + // Kontrollera om TransactionType är en köpnotering + // (ignorera null-värden) + + if (note.TransactionType.ToLower() == "köp") + { + // Skapa en ny aktieinnehavspost baserat på köpnoteringen + + var equity = new StockEquity + { + StockCode = note.StockCode, + Bought = note.TransactionDate, + BoughtPrice = note.StockPrice, + Quantity = note.StockQuantity, + Sold = DateTime.MinValue, + SoldPrice = 0m, + SoldQuant = 0 + }; + _dbContext.Stocks.Add(equity); + await _dbContext.SaveChangesAsync(); + // Debug.WriteLine($"Köpnotering: {equity.StockCode} | {equity.Bought} | {equity.BoughtPrice} | {equity.Quantity} | {equity.BoughtPrice * equity.Quantity}"); + } + + // Kontrollera om TransactionType är en säljnotering + + else if (note.TransactionType.ToLower() == "sälj") + { + if (nyAktie) + { + Debug.WriteLine("<--" + note.ToString()); + + if (note.StockCode != null) + { + equityWarningList.Add(new StockEquity + { + //StockCode = note.StockCode, + //Bought = DateTime.MinValue, + //BoughtPrice = 0, + //Quantity = 0, + //Sold = note.TransactionDate, + //SoldPrice = note.StockPrice, + //SoldQuant = note.StockQuantity + StockCode = note.StockCode, + Bought = note.TransactionDate, + BoughtPrice = note.StockPrice * -1, + Quantity = note.StockQuantity, + Sold = DateTime.MinValue, + SoldPrice = 0m, + SoldQuant = 0 + }); + } + ; + } + else + { + var equityList = await _dbContext.Stocks + .Where(s => s.StockCode == note.StockCode && s.SoldQuant == 0) + .OrderBy(s => s.Quantity).ToListAsync(); + + if (equityList != null) + { + var soldQuant = Math.Abs(note.StockQuantity); + foreach (var eq in equityList.OrderBy(e => e.Quantity)) + { + if (soldQuant == 0) + { + break; + } + else + { + if (eq.Quantity <= soldQuant) + { + eq.Sold = note.TransactionDate; + eq.SoldPrice = note.StockPrice; + eq.SoldQuant = eq.Quantity; + soldQuant -= eq.Quantity; + _dbContext.Stocks.Update(eq); + await _dbContext.SaveChangesAsync(); + //Debug.WriteLine($"Säljnotering: {eq.StockCode} | {eq.Sold} | {eq.SoldPrice} | {eq.SoldQuant}"); + } + else + { + var splitEquity = new StockEquity + { + StockCode = eq.StockCode, + Bought = eq.Bought, + BoughtPrice = eq.BoughtPrice, + Quantity = soldQuant, + Sold = note.TransactionDate, + SoldPrice = note.StockPrice, + SoldQuant = soldQuant + }; + _dbContext.Stocks.Add(splitEquity); + await _dbContext.SaveChangesAsync(); + //Debug.WriteLine($"Säljnotering (split): {splitEquity.StockCode} | {splitEquity.Bought} | {splitEquity.BoughtPrice} | {splitEquity.SoldQuant} | {splitEquity.Sold} | {splitEquity.SoldPrice} | {splitEquity.SoldQuant}"); + eq.Quantity -= soldQuant; + _dbContext.Stocks.Update(eq); + await _dbContext.SaveChangesAsync(); + //Debug.WriteLine($"Säljnotering (split): {eq.StockCode} | {eq.Bought} | {eq.BoughtPrice} | {eq.SoldQuant} | {eq.Sold} | {eq.SoldPrice} | {eq.SoldQuant}"); + soldQuant = 0; + } + } + } + } + } + } + } + } + return equityWarningList; + } +} diff --git a/StockWebHistory/Services/TransactionNotesServices.cs b/StockWebHistory/Services/TransactionNotesServices.cs new file mode 100644 index 0000000..5f7a79b --- /dev/null +++ b/StockWebHistory/Services/TransactionNotesServices.cs @@ -0,0 +1,87 @@ +using Microsoft.EntityFrameworkCore; +using StockWebHistory.Data; +using StockWebHistory.Models; + +namespace StockWebHistory.Services; + +public class TransactionNotesServices : ITransactionNotesServices +{ + private readonly StockWebDbContext _dbContext; + private readonly IServiceProvider _provider; + + + // Parameterless constructor + public TransactionNotesServices(StockWebDbContext dbContext, IServiceProvider provider) + { + _dbContext = dbContext; + _provider = provider; + } + + public async Task> GetAllAsync() + { + return await _dbContext.TransactionNotes + .OrderBy(t => t.StockCode) + .ThenBy(t => t.TransactionDate) + .ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + return await _dbContext.TransactionNotes.FindAsync(id); + } + + public async Task GetByIndexAsync(string stockCode, DateTime transDate) + { + return await _dbContext.TransactionNotes + .FirstOrDefaultAsync(s => s.StockCode == stockCode && s.TransactionDate == transDate); + } + + + public async Task> GetByStockCodeAsync(string stockCode) + { + if (string.IsNullOrEmpty(stockCode)) + { + return new List(); + } + + return await _dbContext.TransactionNotes.Where(t => t.StockCode == stockCode).ToListAsync(); + } + + public async Task> GetAllExistingStockCodes() + { + List stockCodes = new List(); + stockCodes = await _dbContext.TransactionNotes + .Select(t => t.StockCode) + .Distinct() + .ToListAsync(); + + return stockCodes; + } + + + public async Task AddAsync(TransactionNote transactionNote) + { + using var scope = _provider.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + db.TransactionNotes.Add(transactionNote); + await db.SaveChangesAsync(); + } + + public async Task UpdateAsync(TransactionNote transactionNote) + { + _dbContext.TransactionNotes.Update(transactionNote); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(int id) + { + var transactionNote = await _dbContext.TransactionNotes.FindAsync(id); + if (transactionNote != null) + { + _dbContext.TransactionNotes.Remove(transactionNote); + await _dbContext.SaveChangesAsync(); + } + } + +} diff --git a/StockWebHistory/StockWebHistory.csproj b/StockWebHistory/StockWebHistory.csproj index d32f82e..4beaeec 100644 --- a/StockWebHistory/StockWebHistory.csproj +++ b/StockWebHistory/StockWebHistory.csproj @@ -7,8 +7,24 @@ - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Always + diff --git a/StockWebHistory/StockWebHistory.db b/StockWebHistory/StockWebHistory.db new file mode 100644 index 0000000..69ce72c Binary files /dev/null and b/StockWebHistory/StockWebHistory.db differ diff --git a/StockWebHistory/StockWebHistory.db-shm b/StockWebHistory/StockWebHistory.db-shm new file mode 100644 index 0000000..26f7092 Binary files /dev/null and b/StockWebHistory/StockWebHistory.db-shm differ diff --git a/StockWebHistory/StockWebHistory.db-wal b/StockWebHistory/StockWebHistory.db-wal new file mode 100644 index 0000000..a4a2070 Binary files /dev/null and b/StockWebHistory/StockWebHistory.db-wal differ diff --git a/StockWebHistory/Views/DataLoad/Files.cshtml b/StockWebHistory/Views/DataLoad/Files.cshtml new file mode 100644 index 0000000..072fb29 --- /dev/null +++ b/StockWebHistory/Views/DataLoad/Files.cshtml @@ -0,0 +1,25 @@ +@page + +@model FileUploadViewModel + +
+

Ladda upp och processa fil

+ + @if (TempData["Success"] != null) + { +
@TempData["Success"]
+ } + @if (TempData["Error"] != null) + { +
@TempData["Error"]
+ } + +
+
+ + +
+ + +
+
\ No newline at end of file diff --git a/StockWebHistory/Views/DataLoad/Files.cshtml.cs b/StockWebHistory/Views/DataLoad/Files.cshtml.cs new file mode 100644 index 0000000..6a6e718 --- /dev/null +++ b/StockWebHistory/Views/DataLoad/Files.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace StockWebHistory.Views.DataLoad +{ + public class FilesModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/StockWebHistory/Views/DataLoad/Index.cshtml b/StockWebHistory/Views/DataLoad/Index.cshtml new file mode 100644 index 0000000..7f8ff2b --- /dev/null +++ b/StockWebHistory/Views/DataLoad/Index.cshtml @@ -0,0 +1,9 @@ +@{ + ViewData["Title"] = "Dataload Page"; +} + +
+

Welcome to Data Loading

+ File Types + History +
\ No newline at end of file diff --git a/StockWebHistory/Views/Home/Notes.cshtml b/StockWebHistory/Views/Home/Notes.cshtml index b510d1d..0faf500 100644 --- a/StockWebHistory/Views/Home/Notes.cshtml +++ b/StockWebHistory/Views/Home/Notes.cshtml @@ -9,11 +9,11 @@

Stock Notes


-
-

Total Stock Value $@ViewBag.TotalNotes

-
+
+

Total Stock Value @ViewBag.TotalNotes SEK

+
- +
@@ -39,12 +39,12 @@ - + - - - - + + + +
Id@note.TransactionType @note.StockCode @note.StockPrice.ToString("F2")@note.ExchangeRate.ToString("F2")@(note.ExchangeRate == null ? "" : ((decimal)note.ExchangeRate).ToString("F2", System.Globalization.CultureInfo.InvariantCulture)) @note.Currency@note.StockQuantity.ToString("F2")@note.TotalAmount.ToString("F2")@note.Commission.ToString("F2")@note.AmountToPay.ToString("F2")@note.StockQuantity.ToString("F")@(note.TotalAmount == null ? "" : ((decimal)note.TotalAmount).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))@(note.Commission == null ? "" : ((decimal)note.Commission).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))@(note.AmountToPay == null ? "" : ((decimal)note.AmountToPay).ToString("F2", System.Globalization.CultureInfo.InvariantCulture)) Edit Delete diff --git a/StockWebHistory/Views/Shared/_Layout.cshtml b/StockWebHistory/Views/Shared/_Layout.cshtml index d0daa26..d708eb8 100644 --- a/StockWebHistory/Views/Shared/_Layout.cshtml +++ b/StockWebHistory/Views/Shared/_Layout.cshtml @@ -26,6 +26,10 @@ + + diff --git a/StockWebHistory/appsettings.json b/StockWebHistory/appsettings.json index 10f68b8..33f897a 100644 --- a/StockWebHistory/appsettings.json +++ b/StockWebHistory/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Data Source=StockWebHistory.db" + } } diff --git a/StockWebHistory/wwwroot/css/site.css b/StockWebHistory/wwwroot/css/site.css index 819f612..e4d9525 100644 --- a/StockWebHistory/wwwroot/css/site.css +++ b/StockWebHistory/wwwroot/css/site.css @@ -1,3 +1,22 @@ +/* Minskar grundstorleken på all text i hela appen */ +body { + font-size: 0.875rem; /* Motsvarar ca 14px (standard är annars 1rem / 16px) */ +} + +/* Valfritt: Minska rubriker proportionerligt om du tycker de också är för stora */ +h1 { + font-size: 1.75rem; +} + +h2 { + font-size: 1.5rem; +} + +h3 { + font-size: 1.25rem; +} + + html { font-size: 14px; }