diff --git a/Data/FinanceDbContext.cs b/Data/FinanceDbContext.cs index c45e3bf..ed2753e 100644 --- a/Data/FinanceDbContext.cs +++ b/Data/FinanceDbContext.cs @@ -11,17 +11,23 @@ public FinanceDbContext(DbContextOptions options) : base(optio public DbSet Transactions => Set(); + public DbSet SavingsInvestments => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity().Property(transaction => transaction.Amount).HasColumnType("decimal(18,2)"); + modelBuilder.Entity().Property(transaction => transaction.Currency).HasMaxLength(3).HasDefaultValue("USD"); + modelBuilder.Entity().Property(investment => investment.Amount).HasColumnType("decimal(18,2)"); + modelBuilder.Entity().Property(investment => investment.Currency).HasMaxLength(3).HasDefaultValue("USD"); + modelBuilder.Entity().Property(investment => investment.CashImpactType).HasMaxLength(30).HasDefaultValue("ExistingAsset"); var createdDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc); modelBuilder.Entity().HasData( - new Transaction { Id = 1, Type = "Income", Amount = 3000, Category = "Salary", Date = new DateTime(2026, 6, 1), Notes = "Monthly salary", CreatedDate = createdDate }, - new Transaction { Id = 2, Type = "Expense", Amount = 25, Category = "Food", Date = new DateTime(2026, 6, 2), Notes = "Lunch", CreatedDate = createdDate }, - new Transaction { Id = 3, Type = "Expense", Amount = 100, Category = "Shopping", Date = new DateTime(2026, 6, 3), Notes = "Clothes", CreatedDate = createdDate } + new Transaction { Id = 1, Type = "Income", Amount = 3000, Currency = "USD", Category = "Salary", Date = new DateTime(2026, 6, 1), Notes = "Monthly salary", CreatedDate = createdDate }, + new Transaction { Id = 2, Type = "Expense", Amount = 25, Currency = "USD", Category = "Food", Date = new DateTime(2026, 6, 2), Notes = "Lunch", CreatedDate = createdDate }, + new Transaction { Id = 3, Type = "Expense", Amount = 100, Currency = "USD", Category = "Shopping", Date = new DateTime(2026, 6, 3), Notes = "Clothes", CreatedDate = createdDate } ); } @@ -35,6 +41,14 @@ public override Task SaveChangesAsync(CancellationToken cancellationToken = } } + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedDate = DateTime.UtcNow; + } + } + return base.SaveChangesAsync(cancellationToken); } } diff --git a/Migrations/20260606003948_AddSavingsInvestments.Designer.cs b/Migrations/20260606003948_AddSavingsInvestments.Designer.cs new file mode 100644 index 0000000..7f16fa4 --- /dev/null +++ b/Migrations/20260606003948_AddSavingsInvestments.Designer.cs @@ -0,0 +1,132 @@ +// +using System; +using FinanceProject.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinanceProject.Migrations +{ + [DbContext(typeof(FinanceDbContext))] + [Migration("20260606003948_AddSavingsInvestments")] + partial class AddSavingsInvestments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.15") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("FinanceProject.Models.SavingsInvestment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("InvestmentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.ToTable("SavingsInvestments"); + }); + + modelBuilder.Entity("FinanceProject.Models.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("nvarchar(60)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Transactions"); + + b.HasData( + new + { + Id = 1, + Amount = 3000m, + Category = "Salary", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Date = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Monthly salary", + Type = "Income" + }, + new + { + Id = 2, + Amount = 25m, + Category = "Food", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Date = new DateTime(2026, 6, 2, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Lunch", + Type = "Expense" + }, + new + { + Id = 3, + Amount = 100m, + Category = "Shopping", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Date = new DateTime(2026, 6, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Clothes", + Type = "Expense" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260606003948_AddSavingsInvestments.cs b/Migrations/20260606003948_AddSavingsInvestments.cs new file mode 100644 index 0000000..dfc32e5 --- /dev/null +++ b/Migrations/20260606003948_AddSavingsInvestments.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinanceProject.Migrations +{ + /// + public partial class AddSavingsInvestments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SavingsInvestments", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + InvestmentType = table.Column(type: "nvarchar(max)", nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Amount = table.Column(type: "decimal(18,2)", nullable: false), + Date = table.Column(type: "datetime2", nullable: false), + Notes = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SavingsInvestments", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SavingsInvestments"); + } + } +} diff --git a/Migrations/20260606010152_AddCurrencyToFinanceEntries.Designer.cs b/Migrations/20260606010152_AddCurrencyToFinanceEntries.Designer.cs new file mode 100644 index 0000000..674ec0c --- /dev/null +++ b/Migrations/20260606010152_AddCurrencyToFinanceEntries.Designer.cs @@ -0,0 +1,149 @@ +// +using System; +using FinanceProject.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinanceProject.Migrations +{ + [DbContext(typeof(FinanceDbContext))] + [Migration("20260606010152_AddCurrencyToFinanceEntries")] + partial class AddCurrencyToFinanceEntries + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.15") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("FinanceProject.Models.SavingsInvestment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasDefaultValue("USD"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("InvestmentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.ToTable("SavingsInvestments"); + }); + + modelBuilder.Entity("FinanceProject.Models.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("nvarchar(60)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasDefaultValue("USD"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Transactions"); + + b.HasData( + new + { + Id = 1, + Amount = 3000m, + Category = "Salary", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", + Date = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Monthly salary", + Type = "Income" + }, + new + { + Id = 2, + Amount = 25m, + Category = "Food", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", + Date = new DateTime(2026, 6, 2, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Lunch", + Type = "Expense" + }, + new + { + Id = 3, + Amount = 100m, + Category = "Shopping", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", + Date = new DateTime(2026, 6, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Clothes", + Type = "Expense" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260606010152_AddCurrencyToFinanceEntries.cs b/Migrations/20260606010152_AddCurrencyToFinanceEntries.cs new file mode 100644 index 0000000..24c9375 --- /dev/null +++ b/Migrations/20260606010152_AddCurrencyToFinanceEntries.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinanceProject.Migrations +{ + /// + public partial class AddCurrencyToFinanceEntries : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Currency", + table: "Transactions", + type: "nvarchar(3)", + maxLength: 3, + nullable: false, + defaultValue: "USD"); + + migrationBuilder.AddColumn( + name: "Currency", + table: "SavingsInvestments", + type: "nvarchar(3)", + maxLength: 3, + nullable: false, + defaultValue: "USD"); + + migrationBuilder.UpdateData( + table: "Transactions", + keyColumn: "Id", + keyValue: 1, + column: "Currency", + value: "USD"); + + migrationBuilder.UpdateData( + table: "Transactions", + keyColumn: "Id", + keyValue: 2, + column: "Currency", + value: "USD"); + + migrationBuilder.UpdateData( + table: "Transactions", + keyColumn: "Id", + keyValue: 3, + column: "Currency", + value: "USD"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Currency", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "Currency", + table: "SavingsInvestments"); + } + } +} diff --git a/Migrations/20260606012804_AddCashImpactTypeToSavingsInvestments.Designer.cs b/Migrations/20260606012804_AddCashImpactTypeToSavingsInvestments.Designer.cs new file mode 100644 index 0000000..be4b384 --- /dev/null +++ b/Migrations/20260606012804_AddCashImpactTypeToSavingsInvestments.Designer.cs @@ -0,0 +1,156 @@ +// +using System; +using FinanceProject.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinanceProject.Migrations +{ + [DbContext(typeof(FinanceDbContext))] + [Migration("20260606012804_AddCashImpactTypeToSavingsInvestments")] + partial class AddCashImpactTypeToSavingsInvestments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.15") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("FinanceProject.Models.SavingsInvestment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CashImpactType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)") + .HasDefaultValue("ExistingAsset"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasDefaultValue("USD"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("InvestmentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.ToTable("SavingsInvestments"); + }); + + modelBuilder.Entity("FinanceProject.Models.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("nvarchar(60)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasDefaultValue("USD"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Transactions"); + + b.HasData( + new + { + Id = 1, + Amount = 3000m, + Category = "Salary", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", + Date = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Monthly salary", + Type = "Income" + }, + new + { + Id = 2, + Amount = 25m, + Category = "Food", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", + Date = new DateTime(2026, 6, 2, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Lunch", + Type = "Expense" + }, + new + { + Id = 3, + Amount = 100m, + Category = "Shopping", + CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", + Date = new DateTime(2026, 6, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), + Notes = "Clothes", + Type = "Expense" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260606012804_AddCashImpactTypeToSavingsInvestments.cs b/Migrations/20260606012804_AddCashImpactTypeToSavingsInvestments.cs new file mode 100644 index 0000000..7a44de1 --- /dev/null +++ b/Migrations/20260606012804_AddCashImpactTypeToSavingsInvestments.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinanceProject.Migrations +{ + /// + public partial class AddCashImpactTypeToSavingsInvestments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CashImpactType", + table: "SavingsInvestments", + type: "nvarchar(30)", + maxLength: 30, + nullable: false, + defaultValue: "ExistingAsset"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CashImpactType", + table: "SavingsInvestments"); + } + } +} diff --git a/Migrations/FinanceDbContextModelSnapshot.cs b/Migrations/FinanceDbContextModelSnapshot.cs index 2c425d6..bca30b6 100644 --- a/Migrations/FinanceDbContextModelSnapshot.cs +++ b/Migrations/FinanceDbContextModelSnapshot.cs @@ -22,6 +22,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.Entity("FinanceProject.Models.SavingsInvestment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CashImpactType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)") + .HasDefaultValue("ExistingAsset"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasDefaultValue("USD"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("InvestmentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.ToTable("SavingsInvestments"); + }); + modelBuilder.Entity("FinanceProject.Models.Transaction", b => { b.Property("Id") @@ -41,6 +90,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedDate") .HasColumnType("datetime2"); + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)") + .HasDefaultValue("USD"); + b.Property("Date") .HasColumnType("datetime2"); @@ -63,6 +119,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Amount = 3000m, Category = "Salary", CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", Date = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), Notes = "Monthly salary", Type = "Income" @@ -73,6 +130,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Amount = 25m, Category = "Food", CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", Date = new DateTime(2026, 6, 2, 0, 0, 0, 0, DateTimeKind.Unspecified), Notes = "Lunch", Type = "Expense" @@ -83,6 +141,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Amount = 100m, Category = "Shopping", CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Currency = "USD", Date = new DateTime(2026, 6, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), Notes = "Clothes", Type = "Expense" diff --git a/Models/SavingsInvestment.cs b/Models/SavingsInvestment.cs new file mode 100644 index 0000000..68b9ba4 --- /dev/null +++ b/Models/SavingsInvestment.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace FinanceProject.Models; + +public class SavingsInvestment +{ + public int Id { get; set; } + + [Required] + [RegularExpression("Savings|Stocks|Gold|Other")] + public string InvestmentType { get; set; } = "Savings"; + + [Required] + [StringLength(100)] + public string Name { get; set; } = string.Empty; + + [Required] + [Range(0.01, double.MaxValue)] + public decimal Amount { get; set; } + + [Required] + [RegularExpression("USD|INR")] + public string Currency { get; set; } = "USD"; + + [Required] + [RegularExpression("ExistingAsset|NewPurchase")] + public string CashImpactType { get; set; } = "ExistingAsset"; + + [Required] + public DateTime Date { get; set; } = DateTime.Today; + + [StringLength(500)] + public string? Notes { get; set; } + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; +} diff --git a/Models/Transaction.cs b/Models/Transaction.cs index 8ef6daf..a3c7553 100644 --- a/Models/Transaction.cs +++ b/Models/Transaction.cs @@ -7,13 +7,17 @@ public class Transaction public int Id { get; set; } [Required] - [RegularExpression("Income|Expense")] + [RegularExpression("InitialAmount|Income|Expense")] public string Type { get; set; } = "Expense"; [Required] [Range(0.01, double.MaxValue)] public decimal Amount { get; set; } + [Required] + [RegularExpression("USD|INR")] + public string Currency { get; set; } = "USD"; + [Required] [StringLength(60)] public string Category { get; set; } = "Other"; diff --git a/Pages/Analysis.cshtml b/Pages/Analysis.cshtml index 8852d1d..4495925 100644 --- a/Pages/Analysis.cshtml +++ b/Pages/Analysis.cshtml @@ -1,57 +1,218 @@ @page @model FinanceProject.Pages.AnalysisModel @{ - ViewData["Title"] = "Analysis"; + ViewData["Title"] = "Financial Overview"; + ViewData["DashboardSimpleTopBar"] = true; + var selectedCurrency = Model.Analysis.SelectedCurrency; + var selectedMonthLabel = new DateTime(Model.Analysis.SelectedYear, Model.Analysis.SelectedMonth, 1).ToString("MMMM yyyy"); + var expenseLabels = Model.Analysis.CategoryExpenses.Select(item => item.Category).ToList(); + var expenseValues = Model.Analysis.CategoryExpenses.Select(item => item.Amount).ToList(); + var monthlyLabels = Model.Analysis.MonthlySummaries.Select(item => item.Month).ToList(); + var monthlyIncome = Model.Analysis.MonthlySummaries.Select(item => item.Income).ToList(); + var monthlyExpenses = Model.Analysis.MonthlySummaries.Select(item => item.Expenses).ToList(); + var investmentLabels = Model.Analysis.InvestmentTypeSummaries.Select(item => item.InvestmentType).ToList(); + var investmentValues = Model.Analysis.InvestmentTypeSummaries.Select(item => item.Amount).ToList(); + var hasSelectedMonthIncome = Model.Analysis.CurrencySummaries.Any(item => item.MonthlyIncome > 0); + var hasSelectedMonthExpenses = expenseValues.Any(); + var hasMonthlyIncomeOrExpenses = monthlyIncome.Any(value => value != 0) || monthlyExpenses.Any(value => value != 0); } -
-
-

Analysis Page

-

Totals, savings, category expenses, and monthly summaries.

+
+
+
+
+
Welcome back
+

Financial Overview

+
+ + +
+

A simple view of your monthly balance, spending, savings, and net worth.

+

Here is your money summary for the selected month.

+
+
+ +
+
+

View Summary By

+

Choose a month, year, and currency for the dashboard view.

+
+
+ + +
+
+ + +
+
+ + +
+
+ + Reset +
+
- Input
+@foreach (var summary in Model.Analysis.CurrencySummaries) +{ +
+

@summary.Currency Summary - @summary.MonthLabel

+ @summary.MonthLabel +
+
+
+
+

Monthly Income

@FormatMoney(summary.MonthlyIncome, summary.Currency)

@summary.MonthLabel income only
+
+
+
+
+

Monthly Expenses

@FormatMoney(summary.MonthlyExpenses, summary.Currency)

Spending for selected month
+
+
+
+
+

Savings / Investments Added

@FormatMoney(summary.MonthlySavingsInvestmentsAdded, summary.Currency)

Assets added this month
+
+
+
+
+

Available Cash / Bank Balance

@FormatMoney(summary.AvailableCashBankBalance, summary.Currency)

Cash after expenses and new purchases
+
+
+
+
+

Total Savings / Investments

@FormatMoney(summary.TotalSavingsInvestments, summary.Currency)

Assets tracked in this currency
+
+
+
+
+

Total Net Worth

@FormatMoney(summary.TotalNetWorth, summary.Currency)

Cash plus savings and investments
+
+
+
+} +
-
Total Income
$@Model.Analysis.TotalIncome.ToString("N2")
-
Total Expenses
$@Model.Analysis.TotalExpenses.ToString("N2")
-
Savings
$@Model.Analysis.Savings.ToString("N2")
+
+
+

Expense Categories

Selected month spending by category.

+
+ @if (!hasSelectedMonthExpenses) + { +
No @selectedCurrency expenses recorded for @selectedMonthLabel.
+ } + +
+
+
+
+
+

Monthly @selectedCurrency Income vs Expenses

Last 6 months for the selected currency.

+
+ @if (!hasMonthlyIncomeOrExpenses) + { +
No @selectedCurrency income or expenses recorded for the last 6 months.
+ } + else if (!hasSelectedMonthIncome) + { +
No @selectedCurrency income recorded for @selectedMonthLabel.
+ } + +
+
+
+
+
+

Savings / Investments by Type

Assets grouped by category.

+
+ @if (!investmentValues.Any()) + { +
No @selectedCurrency savings or investments recorded through @selectedMonthLabel.
+ } + +
+
+
-
-
+
+
-

Category-wise Expenses

+

Expense Breakdown

@foreach (var item in Model.Analysis.CategoryExpenses) { - + + } + @if (!Model.Analysis.CategoryExpenses.Any()) + { + }
CategoryAmount
@item.Category$@item.Amount.ToString("N2")
@item.Category@FormatMoney(item.Amount, item.Currency)
No @selectedCurrency expenses recorded for @selectedMonthLabel.
-
-
+
+
-

Monthly Summary

+

Last 6 Months Summary

- - +
MonthIncomeExpensesSavings
+ + + + + + + + + + @foreach (var item in Model.Analysis.MonthlySummaries) { - - - + + + + + } + @if (!Model.Analysis.MonthlySummaries.Any()) + { + + }
MonthIncomeExpensesSavings/Investments AddedAvailable CashNet Worth
@item.Month$@item.Income.ToString("N2")$@item.Expenses.ToString("N2")$@item.Savings.ToString("N2")@FormatMoney(item.Income, item.Currency)@FormatMoney(item.Expenses, item.Currency)@FormatMoney(item.SavingsInvestmentsAdded, item.Currency)@FormatMoney(item.AvailableCashBankBalance, item.Currency)@FormatMoney(item.TotalNetWorth, item.Currency)
No @selectedCurrency monthly data yet.
@@ -59,3 +220,28 @@
+ +@section Scripts { + + +} + +@functions { + private static string FormatMoney(decimal amount, string currency) + { + var symbol = currency == "INR" ? "\u20B9" : "$"; + return $"{symbol}{amount:N2}"; + } +} diff --git a/Pages/Analysis.cshtml.cs b/Pages/Analysis.cshtml.cs index 130b2c0..e213f04 100644 --- a/Pages/Analysis.cshtml.cs +++ b/Pages/Analysis.cshtml.cs @@ -1,4 +1,5 @@ using FinanceProject.Services; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace FinanceProject.Pages; @@ -12,10 +13,27 @@ public AnalysisModel(FinanceAnalysisService analysisService) _analysisService = analysisService; } - public AnalysisResult Analysis { get; set; } = new(0, 0, [], []); + [BindProperty(SupportsGet = true)] + public int? Month { get; set; } + + [BindProperty(SupportsGet = true)] + public int? Year { get; set; } + + [BindProperty(SupportsGet = true)] + public string Currency { get; set; } = "USD"; + + public AnalysisResult Analysis { get; set; } = new(DateTime.Today.Month, DateTime.Today.Year, "USD", [], [], [], []); + + public IReadOnlyList Years { get; private set; } = + Enumerable.Range(DateTime.Today.Year - 5, 11).ToList(); public async Task OnGetAsync() { - Analysis = await _analysisService.GetAnalysisAsync(); + Month ??= DateTime.Today.Month; + Year ??= DateTime.Today.Year; + Analysis = await _analysisService.GetAnalysisAsync(Month.Value, Year.Value, Currency); + Month = Analysis.SelectedMonth; + Year = Analysis.SelectedYear; + Currency = Analysis.SelectedCurrency; } } diff --git a/Pages/Index.cshtml.cs b/Pages/Index.cshtml.cs index fd4f189..9aa42fe 100644 --- a/Pages/Index.cshtml.cs +++ b/Pages/Index.cshtml.cs @@ -7,6 +7,6 @@ public class IndexModel : PageModel { public IActionResult OnGet() { - return RedirectToPage("/Input"); + return RedirectToPage("/Analysis"); } } diff --git a/Pages/Input.cshtml b/Pages/Input.cshtml index 853364c..108c964 100644 --- a/Pages/Input.cshtml +++ b/Pages/Input.cshtml @@ -1,85 +1,120 @@ @page @model FinanceProject.Pages.InputModel @{ - ViewData["Title"] = "Input"; + ViewData["Title"] = "Money Entry"; } -
+
-

Input Page

-

Enter income and expense transactions.

+

Money Entry

+

Add income, expenses, or your opening balance.

- Analysis + Dashboard
-
+
-
+
+
+

@(Model.Entry.Id == 0 ? "Add Money Entry" : "Edit Money Entry")

+

Opening balance and income increase your cash. Expenses reduce your cash and net worth.

+
-

@(Model.Entry.Id == 0 ? "Add Transaction" : "Edit Transaction")

-
+ -
- - + +
Opening balance and income increase your cash. Expenses reduce your cash and net worth.
+
-
+
-
- - + @foreach (var currency in Model.Currencies) { - + } +
-
+
+ + + +
+
-
+
- - Clear + + @if (Model.Entry.Id != 0) + { + Cancel Edit + }
-
-
-

Transactions

+
+
+

Money Entries

+

Review opening balances, income, and expenses.

+
+
- - +
DateTypeAmountCategoryNotes
+ + + + + + + + + + + @foreach (var transaction in Model.Transactions) { - - - + + - - + + + } + @if (!Model.Transactions.Any()) + { + + }
DateEntry TypeCategoryAmountCurrencyNotesActions
@transaction.Date.ToString("MMM dd, yyyy")@transaction.Type$@transaction.Amount.ToString("N2")@transaction.Date.ToString("MMM dd, yyyy")@EntryTypeBadge(transaction.Type) @transaction.Category@transaction.Notes - Edit -
- -
+
@FormatMoney(transaction.Amount, transaction.Currency)@transaction.Currency@(string.IsNullOrWhiteSpace(transaction.Notes) ? "-" : transaction.Notes) +
+ Edit +
+ +
+
No money entries added yet. Add your first income, expense, or opening balance.
@@ -88,4 +123,60 @@
-@section Scripts { } +@section Scripts { + + +} + +@functions { + private static string FormatMoney(decimal amount, string currency) + { + var symbol = currency == "INR" ? "\u20B9" : "$"; + return $"{symbol}{amount:N2}"; + } + + private static Microsoft.AspNetCore.Html.IHtmlContent EntryTypeBadge(string? type) + { + var displayText = type switch + { + "InitialAmount" => "Opening Balance", + "Income" => "Income", + "Expense" => "Expense", + _ => "Unknown" + }; + var cssClass = type switch + { + "InitialAmount" => "bg-primary", + "Income" => "bg-success", + "Expense" => "bg-danger", + _ => "bg-secondary" + }; + + return new Microsoft.AspNetCore.Html.HtmlString($"{displayText}"); + } +} diff --git a/Pages/Input.cshtml.cs b/Pages/Input.cshtml.cs index 9dff620..65af2ce 100644 --- a/Pages/Input.cshtml.cs +++ b/Pages/Input.cshtml.cs @@ -18,7 +18,9 @@ public InputModel(FinanceDbContext db) public Models.Transaction Entry { get; set; } = new(); public IReadOnlyList Transactions { get; set; } = []; - public IReadOnlyList Categories { get; set; } = ["Salary", "Food", "Shopping", "Rent", "Utilities", "Travel", "Other"]; + public IReadOnlyList IncomeCategories { get; set; } = ["Salary", "Bonus", "Freelance", "Other"]; + public IReadOnlyList ExpenseCategories { get; set; } = ["Food", "Shopping", "Rent", "Utilities", "Travel", "Health", "Other"]; + public IReadOnlyList Currencies { get; set; } = ["USD", "INR"]; public async Task OnGetAsync(int? id) { @@ -31,6 +33,7 @@ public async Task OnGetAsync(int? id) public async Task OnPostSaveAsync() { + NormalizeEntry(); if (!ModelState.IsValid) { await LoadAsync(); @@ -48,6 +51,7 @@ public async Task OnPostSaveAsync() { existing.Type = Entry.Type; existing.Amount = Entry.Amount; + existing.Currency = Entry.Currency; existing.Category = Entry.Category; existing.Date = Entry.Date; existing.Notes = Entry.Notes; @@ -78,4 +82,14 @@ private async Task LoadAsync() .ThenByDescending(transaction => transaction.Id) .ToListAsync(); } + + private void NormalizeEntry() + { + Entry.Category = Entry.Type == "InitialAmount" + ? "Opening Balance" + : Entry.Category.Trim(); + Entry.Currency = Entry.Currency.Trim(); + Entry.Notes = string.IsNullOrWhiteSpace(Entry.Notes) ? null : Entry.Notes.Trim(); + Entry.Date = Entry.Date.Date; + } } diff --git a/Pages/SavingsInvestments.cshtml b/Pages/SavingsInvestments.cshtml new file mode 100644 index 0000000..12e9792 --- /dev/null +++ b/Pages/SavingsInvestments.cshtml @@ -0,0 +1,267 @@ +@page +@model FinanceProject.Pages.SavingsInvestmentsModel +@{ + ViewData["Title"] = "Savings / Investments"; +} + +
+
+

Savings / Investments

+

Track savings, stocks, gold, and assets without treating them as expenses.

+
+ Dashboard +
+ +
+ @foreach (var summary in Model.Summaries) + { +
+
+
+
+
+ +
+
+
@GetSummaryTitle(summary.InvestmentType)
+
USD: @FormatMoney(summary.UsdAmount, "USD")
+
INR: @FormatMoney(summary.InrAmount, "INR")
+
+
+
+
+
+ } +
+ +
+
+
+
+

@(Model.Entry.Id == 0 ? "Add Investment" : "Edit Investment")

+

Track savings, stocks, gold, and assets without treating them as expenses.

+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+ + @if (Model.Entry.Id != 0) + { + Cancel Edit + } +
+
+
+
+ +
+
+
+
+
+

Savings and Investments

+

Review assets by type, currency, and cash impact.

+
+
+
+ + +
+
+ + +
+
+ + Reset +
+
+
+
+
+
+ + + + + + + + + + + + + + + @foreach (var investment in Model.Investments) + { + + + + + + + + + + + } + @if (!Model.Investments.Any()) + { + + + + } + +
DateTypeNameAmountCurrencyCash ImpactNotesActions
@investment.Date.ToString("MMM dd, yyyy")@InvestmentTypeBadge(investment.InvestmentType)@investment.Name@FormatMoney(investment.Amount, investment.Currency)@CurrencyBadge(investment.Currency)@CashImpactBadge(investment.CashImpactType)@(string.IsNullOrWhiteSpace(investment.Notes) ? "-" : investment.Notes) +
+ Edit +
+ +
+
+
No savings or investments added yet.
+
+
+
+
+
+ +@section Scripts { } + +@functions { + private static string FormatMoney(decimal amount, string currency) + { + var symbol = currency == "INR" ? "\u20B9" : "$"; + return $"{symbol}{amount:N2}"; + } + + private static string GetSummaryTitle(string investmentType) + { + return investmentType switch + { + "Savings" => "Total Savings", + "Stocks" => "Total Stocks", + "Gold" => "Total Gold", + _ => "Total Other Assets" + }; + } + + private static string GetSummaryIcon(string investmentType) + { + return investmentType switch + { + "Savings" => "bi-piggy-bank-fill", + "Stocks" => "bi-graph-up-arrow", + "Gold" => "bi-gem", + _ => "bi-wallet2" + }; + } + + private static string GetSummaryAccentClass(string investmentType) + { + return investmentType switch + { + "Savings" => "asset-summary-savings", + "Stocks" => "asset-summary-stocks", + "Gold" => "asset-summary-gold", + _ => "asset-summary-other" + }; + } + + private static Microsoft.AspNetCore.Html.IHtmlContent InvestmentTypeBadge(string? investmentType) + { + var displayText = string.IsNullOrWhiteSpace(investmentType) ? "Unknown" : investmentType.Trim(); + var cssClass = displayText switch + { + "Savings" => "bg-success", + "Stocks" => "bg-primary", + "Gold" => "bg-warning text-dark", + "Other" => "bg-secondary", + _ => "bg-secondary" + }; + + return new Microsoft.AspNetCore.Html.HtmlString($"{System.Net.WebUtility.HtmlEncode(displayText)}"); + } + + private static Microsoft.AspNetCore.Html.IHtmlContent CurrencyBadge(string? currency) + { + var displayText = string.IsNullOrWhiteSpace(currency) ? "-" : currency.Trim(); + return new Microsoft.AspNetCore.Html.HtmlString($"{System.Net.WebUtility.HtmlEncode(displayText)}"); + } + + private static Microsoft.AspNetCore.Html.IHtmlContent CashImpactBadge(string? cashImpactType) + { + if (string.IsNullOrWhiteSpace(cashImpactType)) + { + return new Microsoft.AspNetCore.Html.HtmlString("-"); + } + + var isNewPurchase = cashImpactType.Trim() == "NewPurchase"; + var cssClass = isNewPurchase ? "bg-primary" : "bg-secondary"; + var text = isNewPurchase ? "New Purchase" : "Existing Asset"; + return new Microsoft.AspNetCore.Html.HtmlString($"{text}"); + } +} diff --git a/Pages/SavingsInvestments.cshtml.cs b/Pages/SavingsInvestments.cshtml.cs new file mode 100644 index 0000000..16f1f85 --- /dev/null +++ b/Pages/SavingsInvestments.cshtml.cs @@ -0,0 +1,128 @@ +using FinanceProject.Data; +using FinanceProject.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace FinanceProject.Pages; + +public class SavingsInvestmentsModel : PageModel +{ + private readonly FinanceDbContext _db; + + public SavingsInvestmentsModel(FinanceDbContext db) + { + _db = db; + } + + [BindProperty] + public SavingsInvestment Entry { get; set; } = new(); + + [BindProperty(SupportsGet = true)] + public string CurrencyFilter { get; set; } = "All"; + + [BindProperty(SupportsGet = true)] + public string TypeFilter { get; set; } = "All"; + + public IReadOnlyList Investments { get; set; } = []; + + public IReadOnlyList Summaries { get; set; } = []; + + public IReadOnlyList InvestmentTypes { get; set; } = ["Savings", "Stocks", "Gold", "Other"]; + + public IReadOnlyList Currencies { get; set; } = ["USD", "INR"]; + + public IReadOnlyList<(string Value, string Label)> CashImpactTypes { get; set; } = + [ + ("ExistingAsset", "Existing Asset / Already Owned"), + ("NewPurchase", "New Purchase / Moved From Bank") + ]; + + public async Task OnGetAsync(int? id) + { + await LoadAsync(); + if (id.HasValue) + { + Entry = await _db.SavingsInvestments.FindAsync(id.Value) ?? new SavingsInvestment(); + } + } + + public async Task OnPostSaveAsync() + { + NormalizeEntry(); + if (!ModelState.IsValid) + { + await LoadAsync(); + return Page(); + } + + if (Entry.Id == 0) + { + _db.SavingsInvestments.Add(Entry); + } + else + { + var existing = await _db.SavingsInvestments.FindAsync(Entry.Id); + if (existing is not null) + { + existing.InvestmentType = Entry.InvestmentType; + existing.Name = Entry.Name; + existing.Amount = Entry.Amount; + existing.Currency = Entry.Currency; + existing.CashImpactType = Entry.CashImpactType; + existing.Date = Entry.Date; + existing.Notes = Entry.Notes; + } + } + + await _db.SaveChangesAsync(); + return RedirectToPage(); + } + + public async Task OnPostDeleteAsync(int id) + { + var investment = await _db.SavingsInvestments.FindAsync(id); + if (investment is not null) + { + _db.SavingsInvestments.Remove(investment); + await _db.SaveChangesAsync(); + } + + return RedirectToPage(); + } + + private async Task LoadAsync() + { + var allInvestments = await _db.SavingsInvestments + .AsNoTracking() + .OrderByDescending(investment => investment.Date) + .ThenByDescending(investment => investment.Id) + .ToListAsync(); + + Summaries = InvestmentTypes + .Select(type => new InvestmentSummary( + type, + allInvestments.Where(investment => investment.InvestmentType == type && investment.Currency == "USD").Sum(investment => investment.Amount), + allInvestments.Where(investment => investment.InvestmentType == type && investment.Currency == "INR").Sum(investment => investment.Amount))) + .ToList(); + + Investments = allInvestments + .Where(investment => CurrencyFilter == "All" || investment.Currency == CurrencyFilter) + .Where(investment => TypeFilter == "All" || investment.InvestmentType == TypeFilter) + .ToList(); + } + + private void NormalizeEntry() + { + Entry.InvestmentType = Entry.InvestmentType.Trim(); + Entry.Currency = Entry.Currency.Trim(); + Entry.CashImpactType = string.IsNullOrWhiteSpace(Entry.CashImpactType) + ? "ExistingAsset" + : Entry.CashImpactType.Trim(); + Entry.Name = Entry.Name.Trim(); + Entry.Notes = string.IsNullOrWhiteSpace(Entry.Notes) ? null : Entry.Notes.Trim(); + Entry.Date = Entry.Date.Date; + } +} + +public record InvestmentSummary(string InvestmentType, decimal UsdAmount, decimal InrAmount); diff --git a/Pages/Shared/_Layout.cshtml b/Pages/Shared/_Layout.cshtml index 64d21a7..5387911 100644 --- a/Pages/Shared/_Layout.cshtml +++ b/Pages/Shared/_Layout.cshtml @@ -3,28 +3,48 @@ - @ViewData["Title"] - Finance Project + @ViewData["Title"] - MoneyMind Tracker + -
- -
+ + + } + else if (!hideTopNav) + { +
+ +
+ }
@RenderBody()
diff --git a/Services/FinanceAnalysisService.cs b/Services/FinanceAnalysisService.cs index 0e7cc6f..5f5683e 100644 --- a/Services/FinanceAnalysisService.cs +++ b/Services/FinanceAnalysisService.cs @@ -3,12 +3,33 @@ namespace FinanceProject.Services; -public record CategoryExpenseSummary(string Category, decimal Amount); -public record MonthlySummary(string Month, decimal Income, decimal Expenses, decimal Savings); -public record AnalysisResult(decimal TotalIncome, decimal TotalExpenses, IReadOnlyList CategoryExpenses, IReadOnlyList MonthlySummaries) -{ - public decimal Savings => TotalIncome - TotalExpenses; -} +public record CategoryExpenseSummary(string Category, string Currency, decimal Amount); +public record CurrencySummary( + string Currency, + string MonthLabel, + decimal MonthlyIncome, + decimal MonthlyExpenses, + decimal MonthlySavingsInvestmentsAdded, + decimal AvailableCashBankBalance, + decimal TotalSavingsInvestments, + decimal TotalNetWorth); +public record MonthlySummary( + string Month, + string Currency, + decimal Income, + decimal Expenses, + decimal SavingsInvestmentsAdded, + decimal AvailableCashBankBalance, + decimal TotalNetWorth); +public record InvestmentTypeSummary(string InvestmentType, string Currency, decimal Amount); +public record AnalysisResult( + int SelectedMonth, + int SelectedYear, + string SelectedCurrency, + IReadOnlyList CurrencySummaries, + IReadOnlyList CategoryExpenses, + IReadOnlyList MonthlySummaries, + IReadOnlyList InvestmentTypeSummaries); public class FinanceAnalysisService { @@ -19,32 +40,168 @@ public FinanceAnalysisService(FinanceDbContext db) _db = db; } - public async Task GetAnalysisAsync() + public async Task GetAnalysisAsync(int month, int year, string? currency) { + var selectedMonth = month is >= 1 and <= 12 ? month : DateTime.Today.Month; + var selectedYear = year is >= 1900 and <= 9999 ? year : DateTime.Today.Year; + var selectedCurrency = currency is "USD" or "INR" ? currency : "USD"; + var selectedMonthStart = new DateTime(selectedYear, selectedMonth, 1); + var selectedMonthEnd = selectedMonthStart.AddMonths(1); + var chartStart = selectedMonthStart.AddMonths(-5); + var transactions = await _db.Transactions.AsNoTracking().ToListAsync(); - var income = transactions.Where(transaction => transaction.Type == "Income").Sum(transaction => transaction.Amount); - var expenses = transactions.Where(transaction => transaction.Type == "Expense").Sum(transaction => transaction.Amount); + var investments = await _db.SavingsInvestments.AsNoTracking().ToListAsync(); + var currencies = new[] { selectedCurrency }; + + var currencySummaries = currencies + .Select(currencyCode => + { + var openingBalance = transactions + .Where(transaction => transaction.Currency == currencyCode && + transaction.Type == "InitialAmount" && + transaction.Date < selectedMonthEnd) + .Sum(transaction => transaction.Amount); + var cumulativeIncome = transactions + .Where(transaction => transaction.Currency == currencyCode && + transaction.Type == "Income" && + transaction.Date < selectedMonthEnd) + .Sum(transaction => transaction.Amount); + var cumulativeExpenses = transactions + .Where(transaction => transaction.Currency == currencyCode && + transaction.Type == "Expense" && + transaction.Date < selectedMonthEnd) + .Sum(transaction => transaction.Amount); + var cumulativeCashReducingInvestments = investments + .Where(investment => investment.Currency == currencyCode && + investment.CashImpactType == "NewPurchase" && + investment.Date < selectedMonthEnd) + .Sum(investment => investment.Amount); + var totalSavingsInvestments = investments + .Where(investment => investment.Currency == currencyCode && + investment.Date < selectedMonthEnd) + .Sum(investment => investment.Amount); + + var monthlyIncome = transactions + .Where(transaction => transaction.Currency == currencyCode && + transaction.Type == "Income" && + transaction.Date >= selectedMonthStart && + transaction.Date < selectedMonthEnd) + .Sum(transaction => transaction.Amount); + var monthlyExpenses = transactions + .Where(transaction => transaction.Currency == currencyCode && + transaction.Type == "Expense" && + transaction.Date >= selectedMonthStart && + transaction.Date < selectedMonthEnd) + .Sum(transaction => transaction.Amount); + var monthlySavingsInvestments = investments + .Where(investment => investment.Currency == currencyCode && + investment.Date >= selectedMonthStart && + investment.Date < selectedMonthEnd) + .Sum(investment => investment.Amount); + + var availableCash = openingBalance + cumulativeIncome - cumulativeExpenses - cumulativeCashReducingInvestments; + var totalNetWorth = availableCash + totalSavingsInvestments; + + return new CurrencySummary( + currencyCode, + selectedMonthStart.ToString("MMMM yyyy"), + monthlyIncome, + monthlyExpenses, + monthlySavingsInvestments, + availableCash, + totalSavingsInvestments, + totalNetWorth); + }) + .ToList(); var categoryExpenses = transactions - .Where(transaction => transaction.Type == "Expense") - .GroupBy(transaction => transaction.Category) - .Select(group => new CategoryExpenseSummary(group.Key, group.Sum(transaction => transaction.Amount))) + .Where(transaction => currencies.Contains(transaction.Currency) && + transaction.Type == "Expense" && + transaction.Date >= selectedMonthStart && + transaction.Date < selectedMonthEnd) + .GroupBy(transaction => new { transaction.Category, transaction.Currency }) + .Select(group => new CategoryExpenseSummary(group.Key.Category, group.Key.Currency, group.Sum(transaction => transaction.Amount))) .OrderByDescending(summary => summary.Amount) .ToList(); - var monthlySummaries = transactions - .GroupBy(transaction => new { transaction.Date.Year, transaction.Date.Month }) - .OrderBy(group => group.Key.Year) - .ThenBy(group => group.Key.Month) - .Select(group => + var monthKeys = Enumerable.Range(0, 6) + .Select(offset => chartStart.AddMonths(offset)) + .ToList(); + + var monthlySummaries = monthKeys + .Select(monthStart => { - var monthIncome = group.Where(transaction => transaction.Type == "Income").Sum(transaction => transaction.Amount); - var monthExpenses = group.Where(transaction => transaction.Type == "Expense").Sum(transaction => transaction.Amount); - var month = new DateTime(group.Key.Year, group.Key.Month, 1).ToString("MMMM yyyy"); - return new MonthlySummary(month, monthIncome, monthExpenses, monthIncome - monthExpenses); + var monthEnd = monthStart.AddMonths(1); + var income = transactions + .Where(transaction => transaction.Currency == selectedCurrency && + transaction.Type == "Income" && + transaction.Date >= monthStart && + transaction.Date < monthEnd) + .Sum(transaction => transaction.Amount); + var expenses = transactions + .Where(transaction => transaction.Currency == selectedCurrency && + transaction.Type == "Expense" && + transaction.Date >= monthStart && + transaction.Date < monthEnd) + .Sum(transaction => transaction.Amount); + var savingsInvestmentsAdded = investments + .Where(investment => investment.Currency == selectedCurrency && + investment.Date >= monthStart && + investment.Date < monthEnd) + .Sum(investment => investment.Amount); + var openingBalance = transactions + .Where(transaction => transaction.Currency == selectedCurrency && + transaction.Type == "InitialAmount" && + transaction.Date < monthEnd) + .Sum(transaction => transaction.Amount); + var cumulativeIncome = transactions + .Where(transaction => transaction.Currency == selectedCurrency && + transaction.Type == "Income" && + transaction.Date < monthEnd) + .Sum(transaction => transaction.Amount); + var cumulativeExpenses = transactions + .Where(transaction => transaction.Currency == selectedCurrency && + transaction.Type == "Expense" && + transaction.Date < monthEnd) + .Sum(transaction => transaction.Amount); + var cumulativeCashReducingInvestments = investments + .Where(investment => investment.Currency == selectedCurrency && + investment.CashImpactType == "NewPurchase" && + investment.Date < monthEnd) + .Sum(investment => investment.Amount); + var totalSavingsInvestments = investments + .Where(investment => investment.Currency == selectedCurrency && + investment.Date < monthEnd) + .Sum(investment => investment.Amount); + var availableCash = openingBalance + cumulativeIncome - cumulativeExpenses - cumulativeCashReducingInvestments; + var totalNetWorth = availableCash + totalSavingsInvestments; + + return new MonthlySummary( + monthStart.ToString("MMM yyyy"), + selectedCurrency, + income, + expenses, + savingsInvestmentsAdded, + availableCash, + totalNetWorth); }) .ToList(); - return new AnalysisResult(income, expenses, categoryExpenses, monthlySummaries); + var investmentTypeSummaries = investments + .Where(investment => currencies.Contains(investment.Currency) && + investment.Date < selectedMonthEnd) + .GroupBy(investment => new { investment.InvestmentType, investment.Currency }) + .Select(group => new InvestmentTypeSummary(group.Key.InvestmentType, group.Key.Currency, group.Sum(investment => investment.Amount))) + .OrderByDescending(summary => summary.Amount) + .ToList(); + + return new AnalysisResult( + selectedMonth, + selectedYear, + selectedCurrency, + currencySummaries, + categoryExpenses, + monthlySummaries, + investmentTypeSummaries); } } diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css index f8d98fc..fba44ed 100644 --- a/wwwroot/css/site.css +++ b/wwwroot/css/site.css @@ -19,4 +19,290 @@ html { body { margin-bottom: 60px; -} \ No newline at end of file + background: #f5f7fb; +} + +.dashboard-hero { + align-items: center; + background: linear-gradient(135deg, #eef5ff 0%, #ffffff 58%, #eaf7ff 100%); + border: 1px solid rgba(13, 110, 253, 0.08); + border-radius: 1.25rem; + box-shadow: 0 0.75rem 2rem rgba(13, 110, 253, 0.08); + display: flex; + gap: 1.5rem; + justify-content: space-between; + padding: 2rem; +} + +.dashboard-hero-action { + white-space: nowrap; +} + +.dashboard-action-card { + color: #212529; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.dashboard-action-card:hover { + box-shadow: 0 0.75rem 1.75rem rgba(13, 110, 253, 0.12); + color: #212529; + transform: translateY(-2px); +} + +.dashboard-action-card .card-body { + align-items: center; + display: flex; + gap: 1rem; +} + +.dashboard-action-icon { + align-items: center; + background: #cfe2ff; + border-radius: 999px; + color: #0d6efd; + display: inline-flex; + flex: 0 0 3rem; + font-size: 1.4rem; + height: 3rem; + justify-content: center; + width: 3rem; +} + +.dashboard-action-arrow { + color: #0d6efd; + font-size: 1.75rem; + margin-left: auto; +} + +.modern-card { + border: 0; + border-radius: 1rem; + box-shadow: 0 0.5rem 1.5rem rgba(33, 37, 41, 0.07); +} + +.modern-card .card-header { + border-bottom: 1px solid rgba(13, 110, 253, 0.1); + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; +} + +.kpi-card .card-body { + align-items: flex-start; + display: flex; + gap: 1rem; +} + +.kpi-card p { + color: #6c757d; + font-size: 0.86rem; + font-weight: 700; + margin-bottom: 0.35rem; +} + +.kpi-card h3 { + color: #212529; + font-size: 1.35rem; + font-weight: 800; + margin-bottom: 0.25rem; +} + +.kpi-card span { + color: #6c757d; + display: block; + font-size: 0.82rem; +} + +.kpi-icon { + align-items: center; + border-radius: 999px; + display: inline-flex; + flex: 0 0 2.75rem; + font-size: 1.35rem; + height: 2.75rem; + justify-content: center; + width: 2.75rem; +} + +.kpi-income .kpi-icon { + background: #d1e7dd; + color: #146c43; +} + +.kpi-expense .kpi-icon { + background: #f8d7da; + color: #b02a37; +} + +.kpi-savings .kpi-icon, +.kpi-investments .kpi-icon { + background: #cfe2ff; + color: #0d6efd; +} + +.kpi-cash .kpi-icon { + background: #e2d9f3; + color: #6f42c1; +} + +.kpi-worth .kpi-icon { + background: #e9ecef; + color: #212529; +} + +.chart-card canvas { + max-height: 260px; +} + +.money-entry-form { + display: grid; + gap: 1rem; +} + +.money-entry-form .form-control, +.money-entry-form .form-select { + min-height: 2.6rem; +} + +.money-entry-table { + min-width: 880px; +} + +.money-entry-table th { + color: #495057; + font-size: 0.82rem; + font-weight: 700; + white-space: nowrap; +} + +.money-entry-table td { + padding: 0.9rem 0.75rem; +} + +@media (max-width: 991.98px) { + .dashboard-hero { + align-items: stretch; + flex-direction: column; + padding: 1.35rem; + } + + .dashboard-hero-action { + width: 100%; + } +} + +.asset-card, +.asset-summary-card { + border: 0; + border-radius: 0.85rem; + box-shadow: 0 0.5rem 1.5rem rgba(13, 110, 253, 0.08); +} + +.asset-summary-card { + overflow: hidden; +} + +.asset-summary-icon { + align-items: center; + border-radius: 999px; + display: inline-flex; + flex: 0 0 2.75rem; + font-size: 1.35rem; + height: 2.75rem; + justify-content: center; + width: 2.75rem; +} + +.asset-summary-amount { + color: #212529; + font-size: 1rem; + font-weight: 700; + line-height: 1.35; + white-space: nowrap; +} + +.asset-summary-savings .asset-summary-icon { + background: #d1e7dd; + color: #146c43; +} + +.asset-summary-stocks .asset-summary-icon { + background: #cfe2ff; + color: #0d6efd; +} + +.asset-summary-gold .asset-summary-icon { + background: #fff3cd; + color: #997404; +} + +.asset-summary-other .asset-summary-icon { + background: #e9ecef; + color: #6f42c1; +} + +.asset-card .card-header { + border-bottom: 1px solid rgba(13, 110, 253, 0.12); + border-top-left-radius: 0.85rem; + border-top-right-radius: 0.85rem; + padding: 1rem 1.15rem; +} + +.asset-form { + display: grid; + gap: 1rem; +} + +.asset-form .form-control, +.asset-form .form-select { + min-height: 2.6rem; +} + +.asset-filter-form { + min-width: min(100%, 360px); +} + +.asset-table { + min-width: 980px; +} + +.asset-table th { + color: #495057; + font-size: 0.82rem; + font-weight: 700; + white-space: nowrap; +} + +.asset-table td { + padding: 0.9rem 0.75rem; +} + +.asset-date-col { + width: 120px; +} + +.asset-name-col { + width: 220px; +} + +.asset-name-cell { + max-width: 240px; + min-width: 180px; +} + +.asset-notes-cell { + max-width: 220px; + min-width: 140px; +} + +.asset-actions-col { + width: 150px; +} + +.asset-badge { + display: inline-block; + min-width: 2.75rem; + padding: 0.4rem 0.55rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; +}