Köp och sälj notor kan läsas in i csv-format och kan sedan listas

This commit is contained in:
2026-09-17 08:51:56 +02:00
parent 02b5fb655a
commit 07dcefa6a4
35 changed files with 1701 additions and 19 deletions

View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.14.37614.0 d17.14 VisualStudioVersion = 17.14.37614.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StockWebHistory", "StockWebHistory\StockWebHistory.csproj", "{76B5FDA3-FD94-4E06-BB2C-8CDDE8C205DD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StockWebHistory", "StockWebHistory\StockWebHistory.csproj", "{76B5FDA3-FD94-4E06-BB2C-8CDDE8C205DD}"
EndProject EndProject

View File

@ -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<IActionResult> 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));
}
}
}

View File

@ -8,6 +8,24 @@ namespace StockWebHistory.Data
{ {
} }
public DbSet<TransactionNote> TransactionNotes { get; set; } public DbSet<TransactionNote> TransactionNotes { get; set; }
public DbSet<StockEquity> StockEquities { get; set; } public DbSet<StockEquity> Stocks { get; set; }
override protected void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TransactionNote>()
.HasIndex(t => new { t.StockCode, t.TransactionDate })
.IsUnique(true);
modelBuilder.Entity<StockEquity>()
.HasIndex(s => new { s.Bought })
.IsUnique(false);
modelBuilder.Entity<StockEquity>()
.HasIndex(s => new { s.Sold })
.IsUnique(false);
modelBuilder.Entity<StockEquity>()
.HasIndex(s => new { s.Bought, s.StockCode, s.Quantity })
.IsUnique(false);
}
} }
} }

View File

@ -0,0 +1,109 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Bought")
.HasColumnType("TEXT");
b.Property<decimal>("BoughtPrice")
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<DateTime>("Sold")
.HasColumnType("TEXT");
b.Property<decimal>("SoldPrice")
.HasColumnType("TEXT");
b.Property<int>("SoldQuant")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("AmountToPay")
.HasColumnType("TEXT");
b.Property<decimal>("Commission")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("ExchangeRate")
.HasColumnType("TEXT");
b.Property<string>("StockCode")
.HasColumnType("TEXT");
b.Property<decimal>("StockPrice")
.HasColumnType("TEXT");
b.Property<int>("StockQuantity")
.HasColumnType("INTEGER");
b.Property<decimal>("TotalAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("TransactionDate")
.HasColumnType("TEXT");
b.Property<string>("TransactionType")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StockCode", "TransactionDate")
.IsUnique();
b.ToTable("TransactionNotes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace StockWebHistory.Migrations
{
/// <inheritdoc />
public partial class initialcreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "StockEquities",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
StockCode = table.Column<string>(type: "TEXT", nullable: false),
Bought = table.Column<DateTime>(type: "TEXT", nullable: false),
BoughtPrice = table.Column<decimal>(type: "TEXT", nullable: false),
Quantity = table.Column<int>(type: "INTEGER", nullable: false),
Sold = table.Column<DateTime>(type: "TEXT", nullable: false),
SoldPrice = table.Column<decimal>(type: "TEXT", nullable: false),
SoldQuant = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StockEquities", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TransactionNotes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TransactionDate = table.Column<DateTime>(type: "TEXT", nullable: false),
TransactionType = table.Column<string>(type: "TEXT", nullable: true),
StockCode = table.Column<string>(type: "TEXT", nullable: true),
StockPrice = table.Column<decimal>(type: "TEXT", nullable: false),
ExchangeRate = table.Column<decimal>(type: "TEXT", nullable: false),
Currency = table.Column<string>(type: "TEXT", nullable: false),
StockQuantity = table.Column<int>(type: "INTEGER", nullable: false),
TotalAmount = table.Column<decimal>(type: "TEXT", nullable: false),
Commission = table.Column<decimal>(type: "TEXT", nullable: false),
AmountToPay = table.Column<decimal>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StockEquities");
migrationBuilder.DropTable(
name: "TransactionNotes");
}
}
}

View File

@ -0,0 +1,109 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Bought")
.HasColumnType("TEXT");
b.Property<decimal>("BoughtPrice")
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<DateTime>("Sold")
.HasColumnType("TEXT");
b.Property<decimal>("SoldPrice")
.HasColumnType("TEXT");
b.Property<int>("SoldQuant")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("AmountToPay")
.HasColumnType("TEXT");
b.Property<decimal>("Commission")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("ExchangeRate")
.HasColumnType("TEXT");
b.Property<string>("StockCode")
.HasColumnType("TEXT");
b.Property<decimal>("StockPrice")
.HasColumnType("TEXT");
b.Property<int>("StockQuantity")
.HasColumnType("INTEGER");
b.Property<decimal>("TotalAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("TransactionDate")
.HasColumnType("TEXT");
b.Property<string>("TransactionType")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StockCode", "TransactionDate")
.IsUnique();
b.ToTable("TransactionNotes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,74 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace StockWebHistory.Migrations
{
/// <inheritdoc />
public partial class ReconcilingMigration : Migration
{
/// <inheritdoc />
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");
}
/// <inheritdoc />
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");
}
}
}

View File

@ -0,0 +1,109 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Bought")
.HasColumnType("TEXT");
b.Property<decimal>("BoughtPrice")
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<DateTime>("Sold")
.HasColumnType("TEXT");
b.Property<decimal>("SoldPrice")
.HasColumnType("TEXT");
b.Property<int>("SoldQuant")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal?>("AmountToPay")
.HasColumnType("TEXT");
b.Property<decimal?>("Commission")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("ExchangeRate")
.HasColumnType("TEXT");
b.Property<string>("StockCode")
.HasColumnType("TEXT");
b.Property<decimal>("StockPrice")
.HasColumnType("TEXT");
b.Property<int>("StockQuantity")
.HasColumnType("INTEGER");
b.Property<decimal?>("TotalAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("TransactionDate")
.HasColumnType("TEXT");
b.Property<string>("TransactionType")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StockCode", "TransactionDate")
.IsUnique();
b.ToTable("TransactionNotes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace StockWebHistory.Migrations
{
/// <inheritdoc />
public partial class decimal_can_be_nullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "TotalAmount",
table: "TransactionNotes",
type: "TEXT",
nullable: true,
oldClrType: typeof(decimal),
oldType: "TEXT");
migrationBuilder.AlterColumn<decimal>(
name: "Commission",
table: "TransactionNotes",
type: "TEXT",
nullable: true,
oldClrType: typeof(decimal),
oldType: "TEXT");
migrationBuilder.AlterColumn<decimal>(
name: "AmountToPay",
table: "TransactionNotes",
type: "TEXT",
nullable: true,
oldClrType: typeof(decimal),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "TotalAmount",
table: "TransactionNotes",
type: "TEXT",
nullable: false,
defaultValue: 0m,
oldClrType: typeof(decimal),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<decimal>(
name: "Commission",
table: "TransactionNotes",
type: "TEXT",
nullable: false,
defaultValue: 0m,
oldClrType: typeof(decimal),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<decimal>(
name: "AmountToPay",
table: "TransactionNotes",
type: "TEXT",
nullable: false,
defaultValue: 0m,
oldClrType: typeof(decimal),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@ -0,0 +1,109 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Bought")
.HasColumnType("TEXT");
b.Property<decimal>("BoughtPrice")
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<DateTime>("Sold")
.HasColumnType("TEXT");
b.Property<decimal>("SoldPrice")
.HasColumnType("TEXT");
b.Property<int>("SoldQuant")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal?>("AmountToPay")
.HasColumnType("TEXT");
b.Property<decimal?>("Commission")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal?>("ExchangeRate")
.HasColumnType("TEXT");
b.Property<string>("StockCode")
.HasColumnType("TEXT");
b.Property<decimal>("StockPrice")
.HasColumnType("TEXT");
b.Property<int>("StockQuantity")
.HasColumnType("INTEGER");
b.Property<decimal?>("TotalAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("TransactionDate")
.HasColumnType("TEXT");
b.Property<string>("TransactionType")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StockCode", "TransactionDate")
.IsUnique();
b.ToTable("TransactionNotes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace StockWebHistory.Migrations
{
/// <inheritdoc />
public partial class decimal_can_be_nullable_more : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "ExchangeRate",
table: "TransactionNotes",
type: "TEXT",
nullable: true,
oldClrType: typeof(decimal),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "ExchangeRate",
table: "TransactionNotes",
type: "TEXT",
nullable: false,
defaultValue: 0m,
oldClrType: typeof(decimal),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@ -0,0 +1,106 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Bought")
.HasColumnType("TEXT");
b.Property<decimal>("BoughtPrice")
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<DateTime>("Sold")
.HasColumnType("TEXT");
b.Property<decimal>("SoldPrice")
.HasColumnType("TEXT");
b.Property<int>("SoldQuant")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal?>("AmountToPay")
.HasColumnType("TEXT");
b.Property<decimal?>("Commission")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal?>("ExchangeRate")
.HasColumnType("TEXT");
b.Property<string>("StockCode")
.HasColumnType("TEXT");
b.Property<decimal>("StockPrice")
.HasColumnType("TEXT");
b.Property<int>("StockQuantity")
.HasColumnType("INTEGER");
b.Property<decimal?>("TotalAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("TransactionDate")
.HasColumnType("TEXT");
b.Property<string>("TransactionType")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StockCode", "TransactionDate")
.IsUnique();
b.ToTable("TransactionNotes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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; }
}

View File

@ -7,12 +7,12 @@ public class TransactionNote
public string? TransactionType { get; set; } public string? TransactionType { get; set; }
public string? StockCode { get; set; } public string? StockCode { get; set; }
public decimal StockPrice { get; set; } public decimal StockPrice { get; set; }
public decimal ExchangeRate { get; set; } public decimal? ExchangeRate { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public int StockQuantity { get; set; } public int StockQuantity { get; set; }
public decimal TotalAmount { get; set; } public decimal? TotalAmount { get; set; }
public decimal Commission { get; set; } public decimal? Commission { get; set; }
public decimal AmountToPay { get; set; } public decimal? AmountToPay { get; set; }
public override string ToString() public override string ToString()
{ {

View File

@ -0,0 +1,32 @@
using CsvHelper.Configuration;
namespace StockWebHistory.Models
{
public class TransactionNoteMap : ClassMap<TransactionNote>
{
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");
}
}
}

View File

@ -1,5 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using StockWebHistory.Data; using StockWebHistory.Data;
using StockWebHistory.Services;
namespace StockWebHistory namespace StockWebHistory
{ {
@ -12,12 +14,24 @@ namespace StockWebHistory
// Add services to the container.x // Add services to the container.x
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<StockWebDbContext>(options => builder.Services.AddDbContext<StockWebDbContext>(options =>
options.UseInMemoryDatabase("StockWebHistoryDB") options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
); );
builder.Services.AddScoped<ITransactionNotesServices, TransactionNotesServices>();
builder.Services.AddScoped<IStockEquityServices, StockEquityServices>();
builder.Services.AddScoped<IFileProcessingService, FileProcessingService>();
var app = builder.Build(); var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<StockWebDbContext>();
db.Database.Migrate();
}
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {

View File

@ -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<FileProcessingService> _logger;
private readonly ITransactionNotesServices _notesServices;
public FileProcessingService(StockWebDbContext db,
ILogger<FileProcessingService> logger,
ITransactionNotesServices notesServices)
{
_db = db;
_logger = logger;
_notesServices = notesServices;
}
public async Task<int> ProcessFileAsync(Stream stream, string fileExtension)
{
var recordsToInsert = new List<TransactionNote>();
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<StockPriceMap>();
}
*/
using (var csv = new CsvReader(reader, config))
{
// Registrera din ClassMap om kolumnnamnen skiljer sig från C#-modellen
csv.Context.RegisterClassMap<TransactionNoteMap>();
// 2. Läsa in posterna asynkront
var records = new List<TransactionNote>();
await foreach (var record in csv.GetRecordsAsync<TransactionNote>())
{
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<bool> 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;
}
}

View File

@ -0,0 +1,134 @@
using StockWebHistory.Models;
using System.Globalization;
namespace StockWebHistory;
public class HandleCsvNotes : IHandleCsvNotes
{
private List<string> lines = new();
private string[] Header_table = new string[0];
public List<TransactionNote> GetTransactionNotes(string filePath)
{
lines = File.ReadAllLines(filePath).ToList();
Header_table = lines[0].Split(';');
var transactionNotes = new List<TransactionNote>();
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<TransactionNote> GetSHBTransactionNotes(string filePath)
{
lines = File.ReadAllLines(filePath).ToList();
Header_table = lines[0].Split(';');
var transactionNotes = new List<TransactionNote>();
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}'");
}
}

View File

@ -0,0 +1,7 @@
namespace StockWebHistory.Services
{
public interface IFileProcessingService
{
Task<int> ProcessFileAsync(Stream stream, string fileExtension);
}
}

View File

@ -0,0 +1,10 @@
using StockWebHistory.Models;
namespace StockWebHistory
{
public interface IHandleCsvNotes
{
List<TransactionNote> GetTransactionNotes(string filePath);
List<TransactionNote> GetSHBTransactionNotes(string filePath);
}
}

View File

@ -0,0 +1,13 @@
using StockWebHistory.Models;
namespace StockWebHistory.Services
{
public interface IStockEquityServices
{
Task<List<StockEquity>> GenerateStockVision();
Task<List<StockEquity>> GetAllAsync();
Task<List<StockEquity>> GetLatestSoldInfoAsync();
Task<List<StockEquity>> GetAllAsync(DateTime equityDate);
Task<List<StockEquity>> GetByCodeAsync(string stockCode);
}
}

View File

@ -0,0 +1,16 @@
using StockWebHistory.Models;
namespace StockWebHistory.Services
{
public interface ITransactionNotesServices
{
Task AddAsync(TransactionNote transactionNote);
Task DeleteAsync(int id);
Task<List<string>> GetAllExistingStockCodes();
Task<List<TransactionNote>> GetAllAsync();
Task<TransactionNote?> GetByIdAsync(int id);
Task<TransactionNote?> GetByIndexAsync(string stockCode, DateTime transDate);
Task<List<TransactionNote>> GetByStockCodeAsync(string stockCode);
Task UpdateAsync(TransactionNote transactionNote);
}
}

View File

@ -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<List<StockEquity>> GetAllAsync()
{
return await _dbContext.Stocks.ToListAsync();
}
public async Task<List<StockEquity>> GetAllAsync(DateTime equityDate)
{
return await _dbContext.Stocks
.Where(s => s.Bought <= equityDate && (s.Sold > equityDate || s.SoldQuant == 0))
.ToListAsync();
}
public async Task<List<StockEquity>> 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<List<StockEquity>> 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();
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
public async Task<List<StockEquity>> GenerateStockVision()
{
var oldStock = "OldStock";
var nyAktie = false;
List<StockEquity> equityWarningList = new List<StockEquity>();
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;
}
}

View File

@ -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<List<TransactionNote>> GetAllAsync()
{
return await _dbContext.TransactionNotes
.OrderBy(t => t.StockCode)
.ThenBy(t => t.TransactionDate)
.ToListAsync();
}
public async Task<TransactionNote?> GetByIdAsync(int id)
{
return await _dbContext.TransactionNotes.FindAsync(id);
}
public async Task<TransactionNote?> GetByIndexAsync(string stockCode, DateTime transDate)
{
return await _dbContext.TransactionNotes
.FirstOrDefaultAsync(s => s.StockCode == stockCode && s.TransactionDate == transDate);
}
public async Task<List<TransactionNote>> GetByStockCodeAsync(string stockCode)
{
if (string.IsNullOrEmpty(stockCode))
{
return new List<TransactionNote>();
}
return await _dbContext.TransactionNotes.Where(t => t.StockCode == stockCode).ToListAsync();
}
public async Task<List<string?>> GetAllExistingStockCodes()
{
List<string?> stockCodes = new List<string?>();
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<StockWebDbContext>();
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();
}
}
}

View File

@ -7,8 +7,24 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" /> <Compile Remove="Services\HandleCsvNotes.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Update="StockWebHistory.db">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,25 @@
@page
@model FileUploadViewModel
<div class="container mt-4">
<h2>Ladda upp och processa fil</h2>
@if (TempData["Success"] != null)
{
<div class="alert alert-success">@TempData["Success"]</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger">@TempData["Error"]</div>
}
<form asp-action="UploadAndProcess" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="fileInput" class="form-label">Välj fil (t.ex. CSV, Excel, JSON):</label>
<input type="file" name="file" id="fileInput" class="form-control" required />
</div>
<button type="submit" class="btn btn-primary">Ladda upp och processa</button>
</form>
</div>

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace StockWebHistory.Views.DataLoad
{
public class FilesModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,9 @@
@{
ViewData["Title"] = "Dataload Page";
}
<div class="text-center">
<h1 class="display-4">Welcome to Data Loading</h1>
<a class="btn btn-primary" asp-area="" asp-controller="DataLoad" asp-action="Files">File Types</a>
<a class="btn btn-secondary" asp-area="" asp-controller="DataLoad" asp-action="History">History</a>
</div>

View File

@ -10,10 +10,10 @@
<h1 class="display-4">Stock Notes</h1> <h1 class="display-4">Stock Notes</h1>
<br /> <br />
<hr /> <hr />
<h3>Total Stock Value $@ViewBag.TotalNotes</h3> <h3>Total Stock Value @ViewBag.TotalNotes SEK</h3>
<br /> <br />
<table class="table"> <table class="table small">
<thead> <thead>
<tr> <tr>
<th>Id</th> <th>Id</th>
@ -39,12 +39,12 @@
<td>@note.TransactionType</td> <td>@note.TransactionType</td>
<td>@note.StockCode</td> <td>@note.StockCode</td>
<td>@note.StockPrice.ToString("F2")</td> <td>@note.StockPrice.ToString("F2")</td>
<td>@note.ExchangeRate.ToString("F2")</td> <td>@(note.ExchangeRate == null ? "" : ((decimal)note.ExchangeRate).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))</td>
<td>@note.Currency</td> <td>@note.Currency</td>
<td>@note.StockQuantity.ToString("F2")</td> <td>@note.StockQuantity.ToString("F")</td>
<td>@note.TotalAmount.ToString("F2")</td> <td>@(note.TotalAmount == null ? "" : ((decimal)note.TotalAmount).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))</td>
<td>@note.Commission.ToString("F2")</td> <td>@(note.Commission == null ? "" : ((decimal)note.Commission).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))</td>
<td>@note.AmountToPay.ToString("F2")</td> <td>@(note.AmountToPay == null ? "" : ((decimal)note.AmountToPay).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))</td>
<td> <td>
<a asp-action="CreateEditNote" asp-route-id="@note.Id">Edit</a> <a asp-action="CreateEditNote" asp-route-id="@note.Id">Edit</a>
<a asp-action="DeleteNote" asp-route-id="@note.Id">Delete</a> <a asp-action="DeleteNote" asp-route-id="@note.Id">Delete</a>

View File

@ -26,6 +26,10 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li> </li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="DataLoad" asp-action="Index">LoadingMethods</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=StockWebHistory.db"
}
} }

View File

@ -1,3 +1,22 @@
/* Minskar grundstorleken p<> all text i hela appen */
body {
font-size: 0.875rem; /* Motsvarar ca 14px (standard <20>r annars 1rem / 16px) */
}
/* Valfritt: Minska rubriker proportionerligt om du tycker de ocks<6B> <20>r f<>r stora */
h1 {
font-size: 1.75rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
html { html {
font-size: 14px; font-size: 14px;
} }