From 492d86db9e54ffa1fdcd1625c577d1e709b902b4 Mon Sep 17 00:00:00 2001 From: Ross Miller Date: Thu, 5 Mar 2026 18:57:12 +0000 Subject: [PATCH] WIP --- .../Services/DashboardServiceTests.cs | 53 ++ BudgetApp/Components/Layout/NavMenu.razor | 6 + BudgetApp/Components/Layout/NavMenu.razor.css | 4 + BudgetApp/Components/Pages/Categories.razor | 6 +- BudgetApp/Components/Pages/Help.razor | 40 +- BudgetApp/Components/Pages/Home.razor | 79 ++- BudgetApp/Components/Pages/Reports.razor | 2 + BudgetApp/Components/Pages/Salary.razor | 264 +++++++++ BudgetApp/Components/Pages/Salary.razor.css | 17 + BudgetApp/Components/Pages/Transactions.razor | 72 ++- BudgetApp/Data/ApplicationDbContext.cs | 21 + BudgetApp/Data/Category.cs | 2 + BudgetApp/Data/Salary.cs | 43 ++ BudgetApp/Data/Transaction.cs | 5 + ...5164332_AddIsPaidToTransaction.Designer.cs | 436 +++++++++++++++ .../20260305164332_AddIsPaidToTransaction.cs | 29 + .../20260305175841_AddSalary.Designer.cs | 514 +++++++++++++++++ .../Migrations/20260305175841_AddSalary.cs | 104 ++++ ...5181148_AddIsDefaultToCategory.Designer.cs | 517 ++++++++++++++++++ .../20260305181148_AddIsDefaultToCategory.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 84 +++ BudgetApp/Program.cs | 1 + BudgetApp/Services/CategoryService.cs | 10 +- BudgetApp/Services/DashboardService.cs | 13 +- BudgetApp/Services/SalaryService.cs | 117 ++++ BudgetApp/Services/TransactionService.cs | 12 + BudgetApp/budgetapp.db | Bin 143360 -> 167936 bytes BudgetApp/budgetapp.db-shm | Bin 32768 -> 32768 bytes BudgetApp/budgetapp.db-wal | Bin 1796352 -> 449112 bytes BudgetApp/wwwroot/js/chart-interop.js | 33 +- 30 files changed, 2473 insertions(+), 40 deletions(-) create mode 100644 BudgetApp/Components/Pages/Salary.razor create mode 100644 BudgetApp/Components/Pages/Salary.razor.css create mode 100644 BudgetApp/Data/Salary.cs create mode 100644 BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs create mode 100644 BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.cs create mode 100644 BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs create mode 100644 BudgetApp/Migrations/20260305175841_AddSalary.cs create mode 100644 BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs create mode 100644 BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.cs create mode 100644 BudgetApp/Services/SalaryService.cs diff --git a/BudgetApp.Tests/Services/DashboardServiceTests.cs b/BudgetApp.Tests/Services/DashboardServiceTests.cs index d11252e..4cadb1e 100644 --- a/BudgetApp.Tests/Services/DashboardServiceTests.cs +++ b/BudgetApp.Tests/Services/DashboardServiceTests.cs @@ -107,6 +107,59 @@ public class DashboardServiceTests Assert.Empty(result.Datasets); } + [Fact] + public void BuildChartData_PaidFutureTransaction_ExcludedFromProjection() + { + var today = DateTime.Today; + var start = today.AddDays(-1); + var end = today.AddDays(5); + + // CurrentBalance is manually maintained by the user — MarkAsPaidAsync never touches it + var account = new Account { Id = 1, Name = "Current", Color = "#000", CurrencyCode = "GBP", CurrentBalance = 1000 }; + var transactions = new List + { + // Unpaid future expense — should reduce the projected balance + new() { Description = "Unpaid Bill", Amount = 100, Date = today.AddDays(3), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = false }, + // Paid future expense — excluded from projection; CurrentBalance already reflects it (user-managed) + new() { Description = "Paid Bill", Amount = 200, Date = today.AddDays(4), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = true } + }; + + var result = DashboardService.BuildChartData([account], transactions, start, end); + + var data = result.Datasets[0].Data; + // Today's index = 1 (start is today-1) + int todayIndex = 1; + // Balance is flat at CurrentBalance up to the unpaid bill + Assert.Equal(1000m, data[todayIndex]); + // Day +3 (index 4): unpaid bill applied → 900 + Assert.Equal(900m, data[todayIndex + 3]); + // Day +4 (index 5): paid bill is excluded from projection → stays at 900 + Assert.Equal(900m, data[todayIndex + 4]); + } + + [Fact] + public void BuildChartData_PaidFutureTransaction_ExcludedFromDayTransactionLabels() + { + var today = DateTime.Today; + var start = today; + var end = today.AddDays(2); + + var account = new Account { Id = 1, Name = "Current", Color = "#000", CurrencyCode = "GBP", CurrentBalance = 500 }; + var transactions = new List + { + new() { Description = "Unpaid", Amount = 50, Date = today.AddDays(1), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = false }, + new() { Description = "Paid", Amount = 100, Date = today.AddDays(2), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = true } + }; + + var result = DashboardService.BuildChartData([account], transactions, start, end); + + // Day +1: unpaid transaction appears in labels + Assert.Single(result.DayTransactionLabels[1]); + Assert.Contains("Unpaid", result.DayTransactionLabels[1][0]); + // Day +2: paid transaction is excluded from labels + Assert.Empty(result.DayTransactionLabels[2]); + } + #endregion #region GetDashboardDataAsync (integration tests) diff --git a/BudgetApp/Components/Layout/NavMenu.razor b/BudgetApp/Components/Layout/NavMenu.razor index a205ebf..a8a16b8 100644 --- a/BudgetApp/Components/Layout/NavMenu.razor +++ b/BudgetApp/Components/Layout/NavMenu.razor @@ -33,6 +33,12 @@ + + + @* ── Salary ── *@ +
+
+
💰 Salary
+
+
+

The Salary page lets you define regular income sources that generate recurring transactions automatically.

+ +
Adding a Salary
+
    +
  • Name — A label for this income source (e.g. "Main Job", "Freelance").
  • +
  • Amount — The net amount credited each payday.
  • +
  • Account — Which account receives the payment.
  • +
  • Recurrence — How often you are paid: Weekly, Monthly or Yearly.
  • +
  • First Payday — The date of the first payment. The day of month (or week) is derived from this and repeats indefinitely.
  • +
+ +
How Salary Transactions Work
+
    +
  • Salary transactions are generated automatically as Income entries up to the end of the current time window, and refreshed whenever you visit or change the date range on any page.
  • +
  • They appear on the Transactions page with a 💰 badge and cannot be deleted from there — manage them on the Salary page instead.
  • +
  • Payday dates are highlighted on the Dashboard balance chart with a 💰 marker and subtle green band so you can see upcoming income at a glance.
  • +
  • The category is automatically set to Salary.
  • +
+ +
Deleting a Salary
+

Deleting a salary removes all future unpaid transactions for that income source. Past transactions remain in your history for accurate record-keeping.

+
+
+ @* ── Categories ── *@
@@ -120,6 +153,7 @@
  • Add a category — Enter a name and pick an emoji icon from the icon picker.
  • Edit — Click the Edit button on a category card to change its name or icon.
  • Delete — Removes the category. Transactions using it will become uncategorised.
  • +
  • Default categories — A set of built-in categories (Groceries, Salary, Rent / Mortgage, etc.) are created automatically and shown with a Default badge. These cannot be edited or deleted.
  • Categories are optional — you can assign them to transactions to get better insight into your spending patterns.

    @@ -175,6 +209,7 @@
    • Use the Dashboard + Add button for rapid entry — the modal stays open after each save so you can batch-add transactions.
    • Set up recurring transactions for regular bills and income to keep your upcoming view accurate.
    • +
    • Use the Salary page for regular pay — it is protected from accidental deletion and highlights paydays on the chart. Use the 90d or 1y window to plan ahead.
    • Assign colours to your accounts to easily distinguish them on the chart and in transaction lists.
    • Hover over elements for tooltips with additional context throughout the app.
    • Click your account balances in the sidebar to quickly navigate to account management.
    • @@ -193,6 +228,7 @@ 📊 Dashboard 🏦 Accounts 💸 Transactions + 💰 Salary 🏷️ Categories 📈 Reports ⚙️ Settings diff --git a/BudgetApp/Components/Pages/Home.razor b/BudgetApp/Components/Pages/Home.razor index cfb5f75..38a6f3d 100644 --- a/BudgetApp/Components/Pages/Home.razor +++ b/BudgetApp/Components/Pages/Home.razor @@ -2,6 +2,7 @@ @attribute [Authorize] @inject DashboardService DashboardService @inject TransactionService TransactionService +@inject SalaryService SalaryService @inject AuthenticationStateProvider AuthStateProvider @inject IJSRuntime JS @implements IAsyncDisposable @@ -195,6 +196,10 @@ else { 🔁 @GetRecurrenceLabel(t) } + @if (t.IsPaid) + { + ✓ Paid + }
    @t.Date.ToString("MMM dd, yyyy") @@ -209,20 +214,30 @@ else
    @(t.Type == TransactionType.Income ? "+" : "−")@t.Account.FormatAmount(t.Amount)
    - @(t.Type == TransactionType.Income ? "Income" : "Expense") -
    - - @if (selectedTransactionId == t.Id) - { -
    - - -
    - } - - } - - @if (RecentTotalPages > 1) + @(t.Type == TransactionType.Income ? "Income" : "Expense") + + + @if (selectedTransactionId == t.Id) + { +
    + @if (t.IsPaid) + { + + } + else + { + + } +
    + + +
    +
    + } + + } + + @if (RecentTotalPages > 1) {
    @@ -282,9 +297,12 @@ else
    @if (selectedTransactionId == t.Id) { -
    - - +
    + +
    + + +
    }
    @@ -446,6 +464,7 @@ else datasets, dayLabels, highlightIndex, + GetSalaryDayIndices(), data.CurrencySymbol); } } @@ -462,6 +481,23 @@ else return data.ChartData.Labels.IndexOf(label); } + private int[] GetSalaryDayIndices() + { + if (data is null) return []; + + var salaryDates = data.RecentTransactions + .Concat(data.UpcomingTransactions) + .Where(t => t.SalaryId is not null) + .Select(t => t.Date.ToString("MMM dd")) + .ToHashSet(); + + return data.ChartData.Labels + .Select((label, index) => (label, index)) + .Where(x => salaryDates.Contains(x.label)) + .Select(x => x.index) + .ToArray(); + } + private async Task SelectTransaction(int id) { selectedTransactionId = selectedTransactionId == id ? null : id; @@ -583,6 +619,14 @@ else pendingDeleteTransaction = null; } + private async Task MarkAsPaid(int id, bool isPaid) + { + if (userId is null) return; + await TransactionService.MarkAsPaidAsync(id, userId, isPaid); + selectedTransactionId = null; + await RefreshData(); + } + private static string GetRecurrenceLabel(Transaction t) { if (t.RecurrenceType != RecurrenceType.None) @@ -609,6 +653,7 @@ else private async Task RefreshData() { if (userId is null) return; + await SalaryService.MaterializeSalaryTransactionsAsync(userId, windowEnd); data = await DashboardService.GetDashboardDataAsync(userId, windowStart, windowEnd); chartDataVersion++; ComputeProjectedBalances(); diff --git a/BudgetApp/Components/Pages/Reports.razor b/BudgetApp/Components/Pages/Reports.razor index 33b9ac8..26f586e 100644 --- a/BudgetApp/Components/Pages/Reports.razor +++ b/BudgetApp/Components/Pages/Reports.razor @@ -3,6 +3,7 @@ @inject TransactionService TransactionService @inject AccountService AccountService @inject CategoryService CategoryService +@inject SalaryService SalaryService @inject AuthenticationStateProvider AuthStateProvider @inject IJSRuntime JS @implements IAsyncDisposable @@ -220,6 +221,7 @@ else { if (userId is null) return; + await SalaryService.MaterializeSalaryTransactionsAsync(userId, dateTo); var transactions = await TransactionService.GetTransactionsInRangeAsync(userId, dateFrom, dateTo); var filtered = transactions diff --git a/BudgetApp/Components/Pages/Salary.razor b/BudgetApp/Components/Pages/Salary.razor new file mode 100644 index 0000000..68dcc42 --- /dev/null +++ b/BudgetApp/Components/Pages/Salary.razor @@ -0,0 +1,264 @@ +@page "/salary" +@attribute [Authorize] +@inject SalaryService SalaryService +@inject AccountService AccountService +@inject AuthenticationStateProvider AuthStateProvider +@rendermode InteractiveServer + +Salary + +

    Salary

    +

    Define your regular income sources. Each salary generates recurring transactions at the set interval, forever.

    + +
    +
    +
    Add Income Source
    +
    +
    + @if (accountsList is not null && accountsList.Count == 0) + { +
    + You need to create an account before adding a salary. +
    + } + else + { + + +
    +
    + + +
    +
    + + +
    +
    + + + @if (accountsList is not null) + { + @foreach (var account in accountsList) + { + + } + } + +
    +
    + + + + + + +
    +
    + + +
    +
    + +
    +
    +
    + } +
    +
    + +@if (salaries is null) +{ +

    Loading...

    +} +else if (salaries.Count == 0) +{ +
    No income sources yet. Add one above to start tracking your salary.
    +} +else +{ +
    +
    +
    Income Sources
    +
    +
    + + + + + + + + + + + + + @foreach (var s in salaries) + { + + + + + + + + + } + +
    NameAmountAccountScheduleNext Payday
    💰 @s.Name+@s.Account.FormatAmount(s.Amount) + + @s.Account.Name + + + 🔁 @s.GetPaydaySummary() + + @if (nextPaydates.TryGetValue(s.Id, out var next) && next.HasValue) + { + @next.Value.ToString("MMM dd, yyyy") + } + else + { + — + } + + +
    +
    + +
    +} + +@if (showDeleteModal && pendingDeleteSalary is not null) +{ + +} + +@code { + private List? salaries; + private List? accountsList; + private Dictionary nextPaydates = []; + private string? userId; + + private SalaryForm formModel = new(); + + protected override async Task OnInitializedAsync() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + await LoadData(); + } + + private async Task LoadData() + { + if (userId is null) return; + + var windowEnd = DateTime.Today.AddDays(90); + await SalaryService.MaterializeSalaryTransactionsAsync(userId, windowEnd); + + salaries = await SalaryService.GetSalariesAsync(userId); + accountsList = await AccountService.GetAccountsAsync(userId); + + nextPaydates.Clear(); + foreach (var sal in salaries) + { + nextPaydates[sal.Id] = await SalaryService.GetNextPaydateAsync(sal.Id); + } + + if (accountsList.Count > 0 && formModel.AccountId == 0) + { + formModel.AccountId = accountsList[0].Id; + } + } + + private async Task AddSalary() + { + if (userId is null) return; + + var entry = new BudgetApp.Data.Salary + { + Name = formModel.Name, + Amount = formModel.Amount, + AccountId = formModel.AccountId, + RecurrenceType = formModel.RecurrenceType, + StartDate = formModel.StartDate, + UserId = userId + }; + + await SalaryService.CreateSalaryAsync(entry, DateTime.Today.AddDays(90)); + + formModel = new SalaryForm + { + AccountId = accountsList?.FirstOrDefault()?.Id ?? 0 + }; + + await LoadData(); + } + + // Delete state + private bool showDeleteModal; + private BudgetApp.Data.Salary? pendingDeleteSalary; + + private void ConfirmDelete(BudgetApp.Data.Salary s) + { + pendingDeleteSalary = s; + showDeleteModal = true; + } + + private void CancelDelete() + { + showDeleteModal = false; + pendingDeleteSalary = null; + } + + private async Task ExecuteDelete() + { + if (userId is null || pendingDeleteSalary is null) return; + showDeleteModal = false; + await SalaryService.DeleteSalaryAsync(pendingDeleteSalary.Id, userId); + pendingDeleteSalary = null; + await LoadData(); + } + + private sealed class SalaryForm : System.ComponentModel.DataAnnotations.IValidatableObject + { + [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Name is required")] + [System.ComponentModel.DataAnnotations.MaxLength(200)] + public string Name { get; set; } = string.Empty; + + [System.ComponentModel.DataAnnotations.Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero")] + public decimal Amount { get; set; } + + public int AccountId { get; set; } + + public RecurrenceType RecurrenceType { get; set; } = RecurrenceType.Monthly; + + public DateTime StartDate { get; set; } = DateTime.Today; + + public IEnumerable Validate( + System.ComponentModel.DataAnnotations.ValidationContext validationContext) + { + yield break; + } + } +} diff --git a/BudgetApp/Components/Pages/Salary.razor.css b/BudgetApp/Components/Pages/Salary.razor.css new file mode 100644 index 0000000..d4c4301 --- /dev/null +++ b/BudgetApp/Components/Pages/Salary.razor.css @@ -0,0 +1,17 @@ +.card { + border-radius: 1rem; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07); + border: none; +} + +.card-header { + border-radius: 1rem 1rem 0 0 !important; + padding: 1rem 1.25rem; +} + +@media (max-width: 767.98px) { + ::deep .table > :not(caption) > * > * { + padding: 0.3rem 0.4rem; + font-size: 0.8125rem; + } +} diff --git a/BudgetApp/Components/Pages/Transactions.razor b/BudgetApp/Components/Pages/Transactions.razor index 983be29..72b1c4e 100644 --- a/BudgetApp/Components/Pages/Transactions.razor +++ b/BudgetApp/Components/Pages/Transactions.razor @@ -3,6 +3,7 @@ @inject TransactionService TransactionService @inject AccountService AccountService @inject CategoryService CategoryService +@inject SalaryService SalaryService @inject AuthenticationStateProvider AuthStateProvider @rendermode InteractiveServer @@ -176,11 +177,28 @@ else @foreach (var t in pagedTransactions) { - - - + + @if (t.SalaryId is null) + { + + } + else + { + 🔒 + } + @t.Date.ToString("MMM dd, yyyy") - @t.Description + + @t.Description + @if (t.SalaryId is not null) + { + 💰 Salary + } + @if (t.IsPaid && t.Date.Date > DateTime.Today) + { + ✓ Paid + } + @t.Account.Name @@ -215,8 +233,21 @@ else
    + @if (t.Date.Date > DateTime.Today) + { + + } - + @if (t.SalaryId is null) + { + + } + else + { + 🔒 + }
    @@ -448,18 +479,20 @@ else private void OnSearchChanged() => currentPage = 1; - private void SetWindow(int days) + private async Task SetWindow(int days) { windowPreset = days switch { 7 => "7d", 30 => "30d", 90 => "90d", 365 => "1y", _ => null }; windowStart = DateTime.Today; windowEnd = DateTime.Today.AddDays(days); currentPage = 1; + await LoadData(); } - private void OnWindowDateChanged() + private async Task OnWindowDateChanged() { windowPreset = null; currentPage = 1; + await LoadData(); } private void OnPageSizeChanged(ChangeEventArgs e) @@ -501,6 +534,7 @@ else { if (userId is null) return; + await SalaryService.MaterializeSalaryTransactionsAsync(userId, windowEnd); transactions = await TransactionService.GetTransactionsAsync(userId); accountsList = await AccountService.GetAccountsAsync(userId); categoriesList = await CategoryService.GetCategoriesAsync(userId); @@ -539,22 +573,29 @@ else private void ToggleSelect(int id) { + var t = transactions?.FirstOrDefault(x => x.Id == id); + if (t?.SalaryId is not null) return; if (!selectedIds.Remove(id)) selectedIds.Add(id); } - private bool IsAllPageSelected() => pagedTransactions.Count > 0 && pagedTransactions.All(t => selectedIds.Contains(t.Id)); + private bool IsAllPageSelected() + { + var selectable = pagedTransactions.Where(t => t.SalaryId is null).ToList(); + return selectable.Count > 0 && selectable.All(t => selectedIds.Contains(t.Id)); + } private void ToggleSelectAll(ChangeEventArgs e) { + var selectable = pagedTransactions.Where(t => t.SalaryId is null).ToList(); if (e.Value is true) { - foreach (var t in pagedTransactions) + foreach (var t in selectable) selectedIds.Add(t.Id); } else { - foreach (var t in pagedTransactions) + foreach (var t in selectable) selectedIds.Remove(t.Id); } } @@ -649,8 +690,10 @@ else { if (userId is null) return; showDeleteModal = false; - foreach (var id in selectedIds) + foreach (var id in selectedIds.ToList()) { + var t = transactions?.FirstOrDefault(x => x.Id == id); + if (t?.SalaryId is not null) continue; await TransactionService.DeleteTransactionAsync(id, userId); } selectedIds.Clear(); @@ -664,6 +707,13 @@ else bulkDeleteCount = 0; } + private async Task TogglePaid(Transaction t) + { + if (userId is null) return; + await TransactionService.MarkAsPaidAsync(t.Id, userId, !t.IsPaid); + await LoadData(); + } + private sealed class TransactionForm : System.ComponentModel.DataAnnotations.IValidatableObject { [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Description is required")] diff --git a/BudgetApp/Data/ApplicationDbContext.cs b/BudgetApp/Data/ApplicationDbContext.cs index 6baa4d0..9edeb55 100644 --- a/BudgetApp/Data/ApplicationDbContext.cs +++ b/BudgetApp/Data/ApplicationDbContext.cs @@ -13,6 +13,7 @@ public class ApplicationDbContext : IdentityDbContext public DbSet Accounts => Set(); public DbSet Transactions => Set(); public DbSet Categories => Set(); + public DbSet Salaries => Set(); protected override void OnModelCreating(ModelBuilder builder) { @@ -45,6 +46,11 @@ public class ApplicationDbContext : IdentityDbContext .HasForeignKey(t => t.ParentTransactionId) .OnDelete(DeleteBehavior.SetNull); + e.HasOne(t => t.Salary) + .WithMany() + .HasForeignKey(t => t.SalaryId) + .OnDelete(DeleteBehavior.SetNull); + e.Property(t => t.Amount).HasColumnType("decimal(18,2)"); }); @@ -55,5 +61,20 @@ public class ApplicationDbContext : IdentityDbContext .HasForeignKey(c => c.UserId) .OnDelete(DeleteBehavior.Cascade); }); + + builder.Entity(e => + { + e.HasOne(s => s.User) + .WithMany() + .HasForeignKey(s => s.UserId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasOne(s => s.Account) + .WithMany() + .HasForeignKey(s => s.AccountId) + .OnDelete(DeleteBehavior.Cascade); + + e.Property(s => s.Amount).HasColumnType("decimal(18,2)"); + }); } } diff --git a/BudgetApp/Data/Category.cs b/BudgetApp/Data/Category.cs index 6e99e45..85037e6 100644 --- a/BudgetApp/Data/Category.cs +++ b/BudgetApp/Data/Category.cs @@ -17,5 +17,7 @@ public class Category public ApplicationUser User { get; set; } = default!; + public bool IsDefault { get; set; } + public List Transactions { get; set; } = []; } diff --git a/BudgetApp/Data/Salary.cs b/BudgetApp/Data/Salary.cs new file mode 100644 index 0000000..c26fc6a --- /dev/null +++ b/BudgetApp/Data/Salary.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +namespace BudgetApp.Data; + +public class Salary +{ + public int Id { get; set; } + + [Required, MaxLength(200)] + public string Name { get; set; } = string.Empty; + + public decimal Amount { get; set; } + + public RecurrenceType RecurrenceType { get; set; } = RecurrenceType.Monthly; + + public DateTime StartDate { get; set; } = DateTime.Today; + + public int AccountId { get; set; } + public Account Account { get; set; } = default!; + + public int? CategoryId { get; set; } + public Category? Category { get; set; } + + [Required] + public string UserId { get; set; } = string.Empty; + public ApplicationUser User { get; set; } = default!; + + public string GetPaydaySummary() => RecurrenceType switch + { + RecurrenceType.Weekly => $"Every week on {StartDate:dddd}", + RecurrenceType.Monthly => $"Monthly on the {GetOrdinal(StartDate.Day)}", + RecurrenceType.Yearly => $"Annually on {StartDate:MMMM d}", + _ => RecurrenceType.ToString() + }; + + private static string GetOrdinal(int day) => day switch + { + 1 or 21 or 31 => $"{day}st", + 2 or 22 => $"{day}nd", + 3 or 23 => $"{day}rd", + _ => $"{day}th" + }; +} diff --git a/BudgetApp/Data/Transaction.cs b/BudgetApp/Data/Transaction.cs index 167c2a0..8cfaf05 100644 --- a/BudgetApp/Data/Transaction.cs +++ b/BudgetApp/Data/Transaction.cs @@ -42,4 +42,9 @@ public class Transaction public int? CategoryId { get; set; } public Category? Category { get; set; } + + public bool IsPaid { get; set; } = false; + + public int? SalaryId { get; set; } + public Salary? Salary { get; set; } } diff --git a/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs b/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs new file mode 100644 index 0000000..877119d --- /dev/null +++ b/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs @@ -0,0 +1,436 @@ +// +using System; +using BudgetApp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BudgetApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260305164332_AddIsPaidToTransaction")] + partial class AddIsPaidToTransaction + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("TEXT"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("BudgetApp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("BudgetApp.Data.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsPaid") + .HasColumnType("INTEGER"); + + b.Property("ParentTransactionId") + .HasColumnType("INTEGER"); + + b.Property("RecurrenceEndDate") + .HasColumnType("TEXT"); + + b.Property("RecurrenceType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ParentTransactionId"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Transaction", b => + { + b.HasOne("BudgetApp.Data.Account", "Account") + .WithMany("Transactions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.Category", "Category") + .WithMany("Transactions") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("BudgetApp.Data.Transaction", "ParentTransaction") + .WithMany() + .HasForeignKey("ParentTransactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("ParentTransaction"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.cs b/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.cs new file mode 100644 index 0000000..9c016c4 --- /dev/null +++ b/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Migrations +{ + /// + public partial class AddIsPaidToTransaction : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsPaid", + table: "Transactions", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsPaid", + table: "Transactions"); + } + } +} diff --git a/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs b/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs new file mode 100644 index 0000000..951ae4a --- /dev/null +++ b/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs @@ -0,0 +1,514 @@ +// +using System; +using BudgetApp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BudgetApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260305175841_AddSalary")] + partial class AddSalary + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("TEXT"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("BudgetApp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("BudgetApp.Data.Salary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecurrenceType") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("UserId"); + + b.ToTable("Salaries"); + }); + + modelBuilder.Entity("BudgetApp.Data.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsPaid") + .HasColumnType("INTEGER"); + + b.Property("ParentTransactionId") + .HasColumnType("INTEGER"); + + b.Property("RecurrenceEndDate") + .HasColumnType("TEXT"); + + b.Property("RecurrenceType") + .HasColumnType("INTEGER"); + + b.Property("SalaryId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ParentTransactionId"); + + b.HasIndex("SalaryId"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Salary", b => + { + b.HasOne("BudgetApp.Data.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Transaction", b => + { + b.HasOne("BudgetApp.Data.Account", "Account") + .WithMany("Transactions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.Category", "Category") + .WithMany("Transactions") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("BudgetApp.Data.Transaction", "ParentTransaction") + .WithMany() + .HasForeignKey("ParentTransactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("BudgetApp.Data.Salary", "Salary") + .WithMany() + .HasForeignKey("SalaryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("ParentTransaction"); + + b.Navigation("Salary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BudgetApp/Migrations/20260305175841_AddSalary.cs b/BudgetApp/Migrations/20260305175841_AddSalary.cs new file mode 100644 index 0000000..358e263 --- /dev/null +++ b/BudgetApp/Migrations/20260305175841_AddSalary.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Migrations +{ + /// + public partial class AddSalary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SalaryId", + table: "Transactions", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + name: "Salaries", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Amount = table.Column(type: "decimal(18,2)", nullable: false), + RecurrenceType = table.Column(type: "INTEGER", nullable: false), + StartDate = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "INTEGER", nullable: false), + CategoryId = table.Column(type: "INTEGER", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Salaries", x => x.Id); + table.ForeignKey( + name: "FK_Salaries_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Salaries_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Salaries_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_SalaryId", + table: "Transactions", + column: "SalaryId"); + + migrationBuilder.CreateIndex( + name: "IX_Salaries_AccountId", + table: "Salaries", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_Salaries_CategoryId", + table: "Salaries", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Salaries_UserId", + table: "Salaries", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_Salaries_SalaryId", + table: "Transactions", + column: "SalaryId", + principalTable: "Salaries", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Transactions_Salaries_SalaryId", + table: "Transactions"); + + migrationBuilder.DropTable( + name: "Salaries"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_SalaryId", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "SalaryId", + table: "Transactions"); + } + } +} diff --git a/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs b/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs new file mode 100644 index 0000000..a622392 --- /dev/null +++ b/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs @@ -0,0 +1,517 @@ +// +using System; +using BudgetApp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BudgetApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260305181148_AddIsDefaultToCategory")] + partial class AddIsDefaultToCategory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("TEXT"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("BudgetApp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("BudgetApp.Data.Salary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecurrenceType") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("UserId"); + + b.ToTable("Salaries"); + }); + + modelBuilder.Entity("BudgetApp.Data.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsPaid") + .HasColumnType("INTEGER"); + + b.Property("ParentTransactionId") + .HasColumnType("INTEGER"); + + b.Property("RecurrenceEndDate") + .HasColumnType("TEXT"); + + b.Property("RecurrenceType") + .HasColumnType("INTEGER"); + + b.Property("SalaryId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ParentTransactionId"); + + b.HasIndex("SalaryId"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Salary", b => + { + b.HasOne("BudgetApp.Data.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BudgetApp.Data.Transaction", b => + { + b.HasOne("BudgetApp.Data.Account", "Account") + .WithMany("Transactions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.Category", "Category") + .WithMany("Transactions") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("BudgetApp.Data.Transaction", "ParentTransaction") + .WithMany() + .HasForeignKey("ParentTransactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("BudgetApp.Data.Salary", "Salary") + .WithMany() + .HasForeignKey("SalaryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("ParentTransaction"); + + b.Navigation("Salary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BudgetApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BudgetApp.Data.Account", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("BudgetApp.Data.Category", b => + { + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.cs b/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.cs new file mode 100644 index 0000000..1e96a91 --- /dev/null +++ b/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BudgetApp.Migrations +{ + /// + public partial class AddIsDefaultToCategory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDefault", + table: "Categories", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDefault", + table: "Categories"); + } + } +} diff --git a/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs b/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs index 7348fe2..92d0001 100644 --- a/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs @@ -127,6 +127,9 @@ namespace BudgetApp.Migrations .HasMaxLength(10) .HasColumnType("TEXT"); + b.Property("IsDefault") + .HasColumnType("INTEGER"); + b.Property("Name") .IsRequired() .HasMaxLength(100) @@ -143,6 +146,47 @@ namespace BudgetApp.Migrations b.ToTable("Categories"); }); + modelBuilder.Entity("BudgetApp.Data.Salary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecurrenceType") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("UserId"); + + b.ToTable("Salaries"); + }); + modelBuilder.Entity("BudgetApp.Data.Transaction", b => { b.Property("Id") @@ -166,6 +210,9 @@ namespace BudgetApp.Migrations .HasMaxLength(200) .HasColumnType("TEXT"); + b.Property("IsPaid") + .HasColumnType("INTEGER"); + b.Property("ParentTransactionId") .HasColumnType("INTEGER"); @@ -175,6 +222,9 @@ namespace BudgetApp.Migrations b.Property("RecurrenceType") .HasColumnType("INTEGER"); + b.Property("SalaryId") + .HasColumnType("INTEGER"); + b.Property("Type") .HasColumnType("INTEGER"); @@ -186,6 +236,8 @@ namespace BudgetApp.Migrations b.HasIndex("ParentTransactionId"); + b.HasIndex("SalaryId"); + b.ToTable("Transactions"); }); @@ -339,6 +391,31 @@ namespace BudgetApp.Migrations b.Navigation("User"); }); + modelBuilder.Entity("BudgetApp.Data.Salary", b => + { + b.HasOne("BudgetApp.Data.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BudgetApp.Data.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("BudgetApp.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + modelBuilder.Entity("BudgetApp.Data.Transaction", b => { b.HasOne("BudgetApp.Data.Account", "Account") @@ -357,11 +434,18 @@ namespace BudgetApp.Migrations .HasForeignKey("ParentTransactionId") .OnDelete(DeleteBehavior.SetNull); + b.HasOne("BudgetApp.Data.Salary", "Salary") + .WithMany() + .HasForeignKey("SalaryId") + .OnDelete(DeleteBehavior.SetNull); + b.Navigation("Account"); b.Navigation("Category"); b.Navigation("ParentTransaction"); + + b.Navigation("Salary"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => diff --git a/BudgetApp/Program.cs b/BudgetApp/Program.cs index 607f44a..f34415c 100644 --- a/BudgetApp/Program.cs +++ b/BudgetApp/Program.cs @@ -81,6 +81,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/BudgetApp/Services/CategoryService.cs b/BudgetApp/Services/CategoryService.cs index 4687073..e7cc5af 100644 --- a/BudgetApp/Services/CategoryService.cs +++ b/BudgetApp/Services/CategoryService.cs @@ -49,6 +49,7 @@ public class CategoryService(ApplicationDbContext db) { Name = d.Name, Icon = d.Icon, + IsDefault = true, UserId = userId }); @@ -57,7 +58,7 @@ public class CategoryService(ApplicationDbContext db) return; } - // Backfill icons on existing default categories that have empty icons + // Backfill icons and IsDefault on existing default categories var defaultLookup = DefaultCategories.ToDictionary(d => d.Name, d => d.Icon); bool updated = false; @@ -68,6 +69,11 @@ public class CategoryService(ApplicationDbContext db) category.Icon = icon; updated = true; } + if (!category.IsDefault && defaultLookup.ContainsKey(category.Name)) + { + category.IsDefault = true; + updated = true; + } } if (updated) @@ -88,6 +94,7 @@ public class CategoryService(ApplicationDbContext db) .FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId); if (category is not null) { + if (category.IsDefault) return; category.Name = name; category.Icon = icon; await _db.SaveChangesAsync(); @@ -100,6 +107,7 @@ public class CategoryService(ApplicationDbContext db) .FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId); if (category is not null) { + if (category.IsDefault) return; _db.Categories.Remove(category); await _db.SaveChangesAsync(); } diff --git a/BudgetApp/Services/DashboardService.cs b/BudgetApp/Services/DashboardService.cs index b260721..836c9b3 100644 --- a/BudgetApp/Services/DashboardService.cs +++ b/BudgetApp/Services/DashboardService.cs @@ -24,12 +24,13 @@ public class DashboardService(TransactionService transactionService, AccountServ var allTransactions = await _transactionService.GetTransactionsInRangeAsync(userId, fetchStart, fetchEnd); var recent = allTransactions - .Where(t => t.Date.Date >= windowStart.Date && t.Date.Date <= DateTime.Today) + .Where(t => (t.Date.Date >= windowStart.Date && t.Date.Date <= DateTime.Today) + || (t.IsPaid && t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date)) .OrderByDescending(t => t.Date) .ToList(); var upcoming = allTransactions - .Where(t => t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date) + .Where(t => t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date && !t.IsPaid) .OrderBy(t => t.Date) .ToList(); @@ -57,8 +58,9 @@ public class DashboardService(TransactionService transactionService, AccountServ { labels.Add(d.ToString("MMM dd")); + // Paid future transactions are excluded — their effect is already in CurrentBalance var dayTxns = transactions - .Where(t => t.Date.Date == d.Date) + .Where(t => t.Date.Date == d.Date && !(t.IsPaid && t.Date.Date > today)) .Select(t => $"{(t.Type == TransactionType.Income ? "+" : "-")}{t.Account.FormatAmount(t.Amount)} {t.Description} ({t.Account.Name})") .ToList(); dayTransactionLabels.Add(dayTxns); @@ -85,8 +87,9 @@ public class DashboardService(TransactionService transactionService, AccountServ } else { + // Future-only window: project forward from CurrentBalance using only unpaid transactions foreach (var t in accountTransactions - .Where(t => t.Date.Date > today && t.Date.Date < startDate.Date) + .Where(t => t.Date.Date > today && t.Date.Date < startDate.Date && !t.IsPaid) .OrderBy(t => t.Date)) { runningBalance += t.Type == TransactionType.Income ? t.Amount : -t.Amount; @@ -95,7 +98,7 @@ public class DashboardService(TransactionService transactionService, AccountServ for (var d = startDate; d <= endDate; d = d.AddDays(1)) { - var dayTxns = accountTransactions.Where(t => t.Date.Date == d.Date); + var dayTxns = accountTransactions.Where(t => t.Date.Date == d.Date && !(t.IsPaid && t.Date.Date > today)); foreach (var t in dayTxns) { runningBalance += t.Type == TransactionType.Income ? t.Amount : -t.Amount; diff --git a/BudgetApp/Services/SalaryService.cs b/BudgetApp/Services/SalaryService.cs new file mode 100644 index 0000000..1e1bc37 --- /dev/null +++ b/BudgetApp/Services/SalaryService.cs @@ -0,0 +1,117 @@ +using Microsoft.EntityFrameworkCore; +using BudgetApp.Data; + +namespace BudgetApp.Services; + +public class SalaryService(ApplicationDbContext db) +{ + private readonly ApplicationDbContext _db = db; + + public async Task> GetSalariesAsync(string userId) + { + return await _db.Salaries + .Include(s => s.Account) + .Include(s => s.Category) + .Where(s => s.UserId == userId) + .OrderBy(s => s.Name) + .ToListAsync(); + } + + public async Task CreateSalaryAsync(Salary salary, DateTime windowEnd) + { + var salaryCategoryId = await _db.Categories + .Where(c => c.UserId == salary.UserId && c.Name == "Salary") + .Select(c => (int?)c.Id) + .FirstOrDefaultAsync(); + salary.CategoryId = salaryCategoryId; + + _db.Salaries.Add(salary); + await _db.SaveChangesAsync(); + await MaterializeSalaryTransactionsAsync(salary.UserId, windowEnd); + } + + public async Task DeleteSalaryAsync(int id, string userId) + { + var exists = await _db.Salaries.AnyAsync(s => s.Id == id && s.UserId == userId); + if (!exists) return; + + await using var transaction = await _db.Database.BeginTransactionAsync(); + + // Delete future unpaid transactions generated by this salary + await _db.Transactions + .Where(t => t.SalaryId == id && t.Date.Date > DateTime.Today && !t.IsPaid) + .ExecuteDeleteAsync(); + + // Unlink past / paid transactions so they stay in history without the salary reference + await _db.Transactions + .Where(t => t.SalaryId == id) + .ExecuteUpdateAsync(s => s.SetProperty(t => t.SalaryId, (int?)null)); + + await _db.Salaries + .Where(s => s.Id == id && s.UserId == userId) + .ExecuteDeleteAsync(); + + await transaction.CommitAsync(); + } + + public async Task MaterializeSalaryTransactionsAsync(string userId, DateTime windowEnd) + { + var salaries = await _db.Salaries + .Where(s => s.UserId == userId) + .ToListAsync(); + + var newTransactions = new List(); + + foreach (var salary in salaries) + { + var latestDate = await _db.Transactions + .Where(t => t.SalaryId == salary.Id) + .OrderByDescending(t => t.Date) + .Select(t => (DateTime?)t.Date) + .FirstOrDefaultAsync(); + + var nextDate = latestDate.HasValue + ? GetNextDate(latestDate.Value, salary) + : salary.StartDate.Date; + + while (nextDate <= windowEnd) + { + newTransactions.Add(new Transaction + { + Description = salary.Name, + Amount = salary.Amount, + Date = nextDate, + Type = TransactionType.Income, + AccountId = salary.AccountId, + CategoryId = salary.CategoryId, + RecurrenceType = RecurrenceType.None, + SalaryId = salary.Id + }); + nextDate = GetNextDate(nextDate, salary); + } + } + + if (newTransactions.Count > 0) + { + _db.Transactions.AddRange(newTransactions); + await _db.SaveChangesAsync(); + } + } + + public async Task GetNextPaydateAsync(int salaryId) + { + return await _db.Transactions + .Where(t => t.SalaryId == salaryId && t.Date.Date >= DateTime.Today) + .OrderBy(t => t.Date) + .Select(t => (DateTime?)t.Date) + .FirstOrDefaultAsync(); + } + + private static DateTime GetNextDate(DateTime from, Salary salary) => salary.RecurrenceType switch + { + RecurrenceType.Weekly => from.AddDays(7), + RecurrenceType.Monthly => from.AddMonths(1), + RecurrenceType.Yearly => from.AddYears(1), + _ => from.AddMonths(1) + }; +} diff --git a/BudgetApp/Services/TransactionService.cs b/BudgetApp/Services/TransactionService.cs index b631057..6bb0eb0 100644 --- a/BudgetApp/Services/TransactionService.cs +++ b/BudgetApp/Services/TransactionService.cs @@ -220,6 +220,18 @@ public class TransactionService(ApplicationDbContext db) await _db.SaveChangesAsync(); } + public async Task MarkAsPaidAsync(int id, string userId, bool isPaid = true) + { + var transaction = await _db.Transactions + .Include(t => t.Account) + .FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId); + + if (transaction is null) return; + + transaction.IsPaid = isPaid; + await _db.SaveChangesAsync(); + } + public async Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null) { var recurringParents = await _db.Transactions diff --git a/BudgetApp/budgetapp.db b/BudgetApp/budgetapp.db index cbaf4a9c13440e48eea51a948604e723a503e1a4..61a2e29d5390569ac5cf39d060f0a60afa46c04c 100644 GIT binary patch delta 6812 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 LVu9(WV3hGckAkLR delta 2291 zcmZ{kYitx%6vywGnY+8Q-I=q6)~1xg_5}sny07li0%E~ZNa&VG*H0L-l!Z2-yR{u4 zMnX$NC?Pb^7`+im07VTN1udbH01^ukF+qIcBN_n{0tqiQ%7)$9;gWAYNTr03@Xmx2#+KOM?Cl5w|bE_HTz=m z!4bTL(*nLgv^H2%8;(>(gP~BQdiE^8zt-+729ea&?Xme%`<#}Ia=x> zohY;q{X7qflydIgMNxG}i(F;A;_=6iv9IctjqJiArI@uZQc&wnPH+yrFc((CP)-OY z!8F4&LeSGkJj3`g?!ZROHor0VnhCSQ_|wy8oG`W;jYhth6OU4x#rhkeR*3*R-vkji z7cnI--q5zjVu>cmQG&>hEQOHAq$EF{&vk4_u9MAiUERC=&WF1q1}kWUsOh1kFkY~{ zHQmwCmRc(b{qDY3`#BPJvrikL8g%w|BZR@l!ZE1vMxB%t#d8-Y6YH$jmP9A3i@|hl z*BiACb}$BWyi{B+c&aVM%lVqtTe9TjyZic2@{)5jN@m1!r7a!lPAeg%QxcB5`}VMi z%9@)X3JTlV1ar&9LwWHa!TWd*f5V%2jlUH?;MaHo&vA!N;4wUcZ{tDk$R=#1DuCjZ z5oI)GG-UM1m?fiIMqNgij7}LHGEx~eP1QjaK~Ys*7fKX7Oz=BA%vXCIS;s9Xr^<9S zu1onZ`xI?Zd-DF&*OqxR-(#mi^A)49N8W_n@L3FFmU-1YZtgbMnopRy#!X|`*u%?N zhAaojuH1%lFf+->Bu^$~Ws*CS^h|PPk~5PWnM7q`m(nl;RC)YAouytpKyUb`(o>Y4qVyD{rzkx|=_yK2QF@Bf zQL8qApzd6?6TX-F>;*WR7nMq=R6?Z^ zDwR;Fgi0k;Dxp#dl}e~oLLHh)fQrC-OGIyp=q=HCYezprZm)6(2lg+i5a-3N=rw8x zpSTZjC$7Lk^H=k%`HI=?_*0 z0zSFd^X-0YFQqfljt$te=0j|xGVoB{Zm|o~nT8$wh{7H_)^9H&%ZByWjAOgUJ)PSQ zo;?m8$Le-$)Sh+OvD^&da5wk&KNdUf;A;EA4m&n%%g&>tED6DAn*QZjEFIT3I~T|fk4dzs4kVZT9chVi?_5?%_5u&O%yyiXO7{)$OC*~ kqH~(Elw*AQ@&&e944*9O|CbW;;R}{RhRYRP4t)go54XVsApigX diff --git a/BudgetApp/budgetapp.db-shm b/BudgetApp/budgetapp.db-shm index 38f52a3b6162b5001e1719cf0d459f728d11f6f3..a92c42380ad4a8939b016adef5a64b4e8b880482 100644 GIT binary patch literal 32768 zcmeI*M{X2R5P;#|#v~iC3ATqEO*R<}m~4^>Cg+^yh&5US*j(2h05b<0CiCaZS+~(HgNTVr|5_i1iWaSy#l0h?NnmBT}8^ z80}qKyzh?qGgsPUc|^)td0+nifA-I+Z?w9~y_T6pB#=`U-a zl*grj0tzUgfC36Apnw7jD4>7>3Mim}0tzUgfC36Apnw7jD4>7>3Mim}0tzUgfC36A zpnw7jD4>7>3Mim}0tzUgfC36Apnw7jD4>7>*#fQ1Wg|Q2;|Ql2;tG#>nfukrq9iBsN}#p&s5;?(r*oZ>updBlsxgQ%m9+nC2DcCweF403_1JmFOx z^_O2OU8Oky`Q zn+5c;oBa%MmP_2=8E+<6iB_Y40tzUgfC38SBruJIG1KoR_jAf;8U<=cU^+$P%iQ5PZ%H$zT?#1B R=mJw%QkwAI=nntx|2Ji)Oq~D# literal 32768 zcmeI*b#N9(7{~GN4|kW~!8N#haEIV70fM^(cXxMp2o6;$)TvQ-r={++rA-Cy+TKj& zCTZUE4;s?vW;T1bxBKkwXE&3Xcjny$T?xUxk~v9`<-GHwsB^z2Ji67Th_E3GE{u(w zdofjL)b-2J*Rov=4N72rH@17Zac{)9{wjsDPPu!z{l`qXoabOC6?T$4o^(zMq%4p+ zfyVO2^KWDN+j;QG+ZMzQlm6l(==T(G*h!QTL*PWhjm296s6-jp;J1oGdinta!--5 z%LGio1WdpLOuz(8zywUd1WdpLOuz(8zywUd1We%nA<#mTHC0>fli_%mJ1v=G!r<$Q7-cWukserBvU43S3Z?e4b@esnyIDQDN=JZUrV%7>$F+hwA-}f z)xI?qQaPttXCVi9DCnH;T$=J!p$7G7OmkY(o=$Y57yTH>5JoVTNlaxXb6LnzRlD34=g&y=_0D~FED8>`XbY?T3#Vltv>)Fh9cC()&oZuYy^BAx2ChzefKk^4s zDy3F>Wm9eiskq9iqN=No8mg(lGhhvZ#;XDOB+GWy?~y{XtQ~FXMvHcw(_1ZQ1%4sLD7y_ zTYG`A>(|vCF9T=B!;x^j;7jSY4U;`CZ45mpSI{Y%XPMKf&gNiqfsAjtpRUCI{8cRV zJ%aQ9ZPnPz29DlWZoH@!8Jjd?oAC!@lkpo~1~(7@0T2KI5C8!X009sH0T2KI5CDOH z0s*h?9Ono%&54IvV{?SX5Nr^>4_iJ!cb?`@9{N@pa}Hxu8XDRUxqpO5?Te7U~BW18`pHiG`! zq~8_Tts_uX zZHi?BXypLiTgVIvI+aoUbW*-R%@aL#0fe2swn0?N9BUoGDC@RuI{T{+erJQAK}v=IbA00ck)1V8`; zKmY_l00cnbs3dSu>kh6y)O82NdV;C%5p4Y9N$39PxdC@@vtW$-CC&KM_|SOQc*}U* zc*S_Z_&H@QeB5}<*l0Xx+-s~e))?O~Rv3$oxY24f8FP%U8DBNNY)msI8RLwx#_5J> zcnyzHY81Ku;r^@p1NYnR-@9L>!{P=4AOHd&00JNY0w4eaAOHd&00O;5z~y8PovoI# zO3F$pE2J!!vP{bLQkF_tBIPbEM3ca)6@BX>_r|$oBZ#H%tZ_vDfUm5>pJYj4$9yK1K zSp#<(tBo6trN%-dN^=Hg8&?{a85bJo7_~HGV3cvPaf0DCT<*_kzQFtL-R?Ksueg8J zE6+O)3IZSi0w4eaAOHd&00JNY0w8eA5YU~PgRK_(RbszV>{s-2IvlK%El=vQq+XxY zrAb|q)a#PEIH}hrwJWLDBy~|z7Yc1*@uYSnwLPf|k{U~DTT-J*ZB1$RMFR5Xn zvsiOdo01wzYGYCxl3JhCU{dEMbxu-eC-v&2UL`1XihYaN^Z5cJdACW;7ce)szWOs` zdW-Z0_9cA*<85P?@ftY-zczkh{ER$-EyfS%y95sz_Z#bt+sPNWnZ8YM9XSIrW1i7S z-oV$4D~wCY9XQXJXw;BDaE5V;F_avF8ZFBSVGVt<|3FBbc2#lB1IuMzu2V!u%A z|A*_E(F&^a!{w;N!n3U%>a}KW$hyXiB)89vtJ^tr<_!8UjJ1%5b`0c0cG| z=)TB3xctxMkC%U|{G#$fWq&C9e%bY9)60gG{<-u=r8kt$C>_!NgZ}sQzqo>LEKsqdLAOHd&00JP;hXlrSyUR7^VAbAYO)F-_ zx>4t?@>kVX_{UXLRhxeQMDa5v5{U)79Amo6bCj;JEM1nPbhSn4(cO;yN^8Y>>6TF9F2#f!DvH> zbx!WqY~!o;Z=hBDitBCTEAz%L%^hD`QJKH~CARVT>%Y!6-k-Pqi|yk56Y|DiYa2g4 zZ+w?+d|lr7Yi#3d^Tsc-jaM!n%l@;_HeR`$EaER;(V2?Zs##Y5r;FFV|65!m&a+m; z>4@|~Fg$rh=>t6${Dc+Eyc#fmj=P0gp#>~-!*2UGHm?=ItM{%VaW{S_r zDy}F0BrAO|Q+#%g;z|e16u&x0ai#ZVieHtZxYG4f#m`wWD^;BLJL)s3wkxlVvbaTO za?+bJ#TOX!I$QTnV9OBr0)6;)6;*=(2!H?x^fG~ejxSJ>=z7UN$l=rQc{NM{H7a@FsY)6EyLS-nH9=UQy1ipac9>EtNCtWpjQ{&S! zSHKtW9}T|1su$mSaOakbboc_j{FW4l2LTWO0TAdz0`LVa=Nl*=ELn~#F<-!LT#oqy z`%Sj7>_3<%+H#s2T)700ck)1V8`;KmY_l00cnbXeYoI z44^!Pn#M-yY!%6HovkDptg{s)19Y~W5Ny@iGAY+fSt@0TlE-BYYStMnl zlunY(I*XIk>#RdcyOae|Vp7_qM5VM!iAb3*WuBC<6rMC-mM&8r$i`2XjlWQ5yz%Gi zj5mI~&UoW1b;cV%T4%iRBX!0bKU8PD@q={68}HT`Z+t(U@y46wT-o^Xlw^_aRm#SXrX-7WZ=`JeP)f2$_Xf$v zyD7;c-RmbCujDD*_STiPkIujS!?Z83*=@i5%H4yFD#r~3KmY_l00ck)1V8`;KmY_l z00eqWK&+_GNB-xFf&V#T+&^9n`>S}KLOSr3 z`2hMgQa=2Dm6Q*mcam}g;1yCnh<=%rTL9Y`lw23rAN=uru`K0xv^GN5ChDm3$X4Y)%)vjbstcmn8Ci4yOI;V;nC-M3O z0e+rS#aT`g=U6Mw&@aw!v^cv{#krj<&TOzaFON8@GI35WaYjm>!V$}U@!PqPiH&Jr zV4u4eHflF+AOHd&00JNY0w4eaAOHd&00JP;w*-=T3aJsFIk>%`eL{H(N%nAmL3>BI z3kfmmPKZ%=LX5f-V$_`wqwa(lbtlBAJ0V8h2{GzUh*5V!jJgwy&m_28%peGe2?PN# ze;^>H4+O;QfqfFsrVRub|5CwaOy(*4VtfH5AHk28 zUvcZIAMW_PoT}mL3%GXo#>Nkh8wh{^2!H?x^c8`FTX&Gs3cP2X)lkV5l*}+drJsiA6?p=2NDvNdBG)?cai_f?2AjL1Ti&xfBv#!50XMFb3*4FVI zcJT$&zrZeDS&7}c{x+LUss8^3i07>%l>VwlJZBM+eTAqI z&ss;=)bWgUgmM+Abv$huq4s+rBCzO6<)X&s^TW;J4~b%fG?)rhk* zDPrkWI4ebFc6T|T>r$tAB-!yTqm`~>6&V8&Rvf>ZYrv4`=4)2|LV{OFG=|V#(x>yCBY2@KmY_l00ck)1V8`;KmY_l00cmw zR|uS2)K44c7&7ztK&T^bR#sIzHjQkktPIxFH2RwwDr+b)WPM|0V?|AMQ(Z-ULv3S4 z<%IE}riOZYG}L&})URk0_3?T??f7DTj2Tl$R@T>s8mbBn*0dA(X{CID_1`)x^1VlY z6i9uKz}R6t+AF7xgMt7EfB*=900@8p2!H?xfB*=900WZpr)9;@sespgk4g#IZD1JIAU*L^Jj|4yI`s?lK z`2u_U_GD2z2!H?xfB*=900@8p2!H?xfB*=9KyMJxos8)YI`ZlK=qX>o`<2+1pMRIF zfG^M+%yCQ*009sH0T2KI5C8!X009sH0T4J`2ykD(%YA_}=+~F>1)hKXjeVOxIPs3O zFROj$$zlKZXEzURJMH*1>nj1zg&!cFl4o3)zK!8<l-~7RrTPW_woWli;oejZwxGh>p=$`x5uBX5i!)6XCnbFb~i-n`j zg~femm8|ok)2YrGo%J0J?co@&w!jlz`+cgn{}3J=Tu8MR7V*i6jtLc4^oX|IGYQi@lbm_ z7>>4vqVd9lS8t@g`ThuM?gjKrMw`vcI}40lwNzv=k`-c=uc_~}nBeKa*+ zpgd;3Q7mpC00JNY0w4eaAOHd&00JNY0tZ2$&<}D=8oC!`6VRy6aZVv)-jk`|ph5}` zqha-Xg%li01-BJaa0nG#Ur51|sNjM^3VNyF#f20cOa+G*Qjlls`)eTu`Lg`a7gCUa z4*x(Q1qV{W#f20+o(f)3NI?%3^c7NY02LG#Kx)3gh}Ns_**fdQXUdHiwIXAaW^6P5 zU~DpeL*;M-0T2KI5C8!X009sH0T2KI5C8!X_-7FC>dtYFP}7`vs5Lf6Og#uTi1{FF z`2gK{nnQVLbs2LGV^bO$+B&204&C@jD=Kr*BQt_Dse>jSINT2B2sSMk4>h;(`&D!X ztyQpzc)J6At?80Cgi};6Z;KVDc$0?GU~WP&Y!>+mmnfDBmM^vHFTPT`NNg zK8e6nezIolFdj8-Fs?GHjsEW6ySKP+A$MT1dtmvy1V8`;KmY_l z00ck)1V8`;j#>hR&ibz^v|weS1^tB;2DJ$ zJiX9@qY5o}TA>9`EwtdsLJOW!Xu%P5=_=nNc;?OD{qoa2?|v8GBRFbb)No=T00JNY z0w4eaAOHd&00JNY0*5<+L--zn>)^jfP|9(n))!du^sE7&Pnprlzem7~pJ~R=j1`AF zF0>B>KmY_l00ck)1V8`;KmY_l00fR)0_PU>(}p>Q%sf62>WG__Rn?A7BO59!gEcjU zd_m`;sbA41y83CyizCdK>U?`-Wqoa^p{meeP2(#rrt=Z_KDZ}*_G2|a=aUGSu|s^1 z;K*f*mV*EYfB*=900@8p2!H?xfB*=9z|lmY(60v+6KA^wXaY-h1ix5%?a# z(bR8nE+7B`AOHd&00JNY0w4eaAOHeK6oD`Fdj!sW`3O$oY*ODNxakuA;D7n^FCLQL zBX~?R9@F?51Sb9R1dX_X00@8p2!H?xfB*=900@8p2!H?x^eur)oF$sq!KxXXIVBQ_ z1-pzoXO+LIw!&XsQB`gF{S(EH%U8%(3FuTt@zY890<%Va^!NC#u{X-^5$x5By?uMK zs2v1A00ck)1V8`;KmY_l00ck)1VErS2HO#^U!dXl|M>NDTQ^?_U!XUb zWZwBzmt@u*JY3p~ zc7Ol~fB*=900@8p2!H?xfB*=900g!S-;d zBk#I{Vm-ms_XwN={&d;0lbyS{S>QC@)r?P#4~=)-FMYAw7Yz)@0|5{K0T2KI5C8!X z009sH0T2LzGy#{BIdryK)7U7Tts)t&vy~)+b+&?JfXzJOQy0;co@Oz8`lVrae96hmtg;R~2zXifKoFJOwHHQf`wfXRd}U^3wgm`wNr zCKJAZR}S7i!X9|oBkX|<9>(o~^&ZCUfwdmS?SYjZ#_fR{JdE1|OFfL+1B*P2+XFEV zF z_!##EM$m8C7ij$T&o|xs<(jWbU*LVpM__zxyzkzebNiLM2OF;uHxK{;5C8!X009sH z0T2KI5C8!X=qUl+sX4^R|9mmQ( zP8Bsy;`NEg_|$=^;w&eLbF39-=ojZVTAbaf;@nOaXEvC1iKtt}evR03UtlEtrhS3G ze|Y=lU-`GY{**OeV4wR7-FS_-fdB}A00@8p2!H?xfB*=900@9UpAz7op9^@Z z1?>|D_ZPH1++Wb%5$-}ljJgwI)SVEc?t~b1C&Z{bAx7N^G3rhTHz6UsgoJPs62eDF zh?xX;ix~s~F@Yc;<_`qK^nrkwJrEF+2LfX5KtN0#2#A>j0WonPAm$AO#I%6`<6kPc zj5YVb7vSgXl=B6ga=w65%op&{5!3ky26vyn=Ip(H|9v?<=rk5;#&+WYW1(yJ7rW8H zaRUJm009sHfj%QJrrTYvF$b&m7He8DE7pxVZ{T`zMN@DUnDl*yW%i z?^&0wu`d0DEM1ay>}u=MYcj{yiuKYh$(B@8W3pO8$7|47KURHKu|q4?O7!#Mr91jh zjy+&fz2<0kJ>v-BSLhbiz#5U^)rKhVAPgzGOeOrxq z(mF!v&1%F}>j*Gy01mUV7AyPZ{ZtzERz1uderzu84A-O(!gal2@xYf|)> z?g7gC5tmWt%lUr9Ii~x#9Ho^PQ2FkKN*g&!D=(MH()5;0@dfnR6F-bR(f=O!0)6&o z6%~U32!H?x^g03f0+zml{o7OFE5H}X<;&2x6u!WIrrH5-mPP-8FHoYVy$5AbrH(`4 z3&>X}$%?KmWgw=qf-hiq*^#RNUm)Abp$>KI`2zBDij3z_h%fNg-}KpAH&hG)AOHd&00JNY0w4eaAOHd&00PGv0k7^H=Lj{;iHBNaa{}$b zXh*Oi9&U?vu;l}E=V=b*q4j0VIgCwdXlUz<#yecbM_N&tiyoN~j0D@mp$@0p;T*xH z1>>RSHh#Z~XqK0x%F~?_INm3B?t0X5Q&HX5Z;jpg-`@Ve=Vn2fu~9QVH9j=nHQq8_ zH(oJbFn(@4Wjt;?W^8o7bSN9I5jPM30T2KI5C8!X009sH0T2KI5a<;Ge0@D#<|))= zotcDqdR?q9NWv2olJEqDBs@VO2~SW+!V?sdv`XO#3h5qCP)IUQN>~bCYH*e= zQy{w7dVYQ`c9)cIOIau7PAPXtxn0UyDYr?vRf@<{=o0G=nsoTIFK}JuZ!h}O^0JHb z`2xm9xw${+$;NBM4Fo^{1V8`;KmY_l00ck)1V8`;dW8TV_F_?Ykod`$QPJ|=tt9}~WSPv#@=$$SJp;R}r5&q8XxK=*~CUVZkf zBWFrqV2ft#H9j`pH{LdO8L!cdfnOWHFn(q{VQg`4PHw+)_h9Rl=N$(H0T2KI5C8!X z009sH0T2KI5IANCh%9}pX+?qa#lZg@G43BPhW%BnQ;hrHCS68Ac9ULD`WER@(p{uW zNZ%yAj`R)E#iXy1UQ7BaX&32E(rZXxAzeiJGU-Cn?W~2xjgPfBX$NULX*+3*bOC9U zG)5XBZ6lpW8YOKZZ6$3YjgV5R6E>f;o^&4RT+%SBI3kl&ZB!sh&5WYe}xC#m3DI|oWkPv=CLbwSD;Uy%5laLTTLPE?WxLeF1 z2#5&;0Wp6dAf^ul#O#59m^=^=a|Z%q>Oeru90-Vs0|7B_Ai%=B!A?0}z$xboIOTi+ zr<^a~l=B6ga=w65&KGdX`2tQkU%)Bn3pnL`0jHQRpyVm6YM%St`9C#3=lKZA%6DqU zcH;qKp>eTslKVaP6Ye$cdUuW6Rld`;`%pGIIBp;S0w4eaAkb$7kdFZQ2o7A}8u=K@dd<@OWwA=^wA6g(B`3Ow0!2U$PUtMD#`3SPI4k}6FR947G&>Q&(#&n-#mBpTt zVkpUJ(>d?GIZ7)jG}5I9Ta~V@sI)#dCB0(~R_*0k23T=AZ(gPKu}{cRT1g(0Zpk3) z(rOCXbm@UPN-NznUHbSOrIi$#>C&DYrEB+pkxiE#kfXHnlA11kT#nK;md7@7l+KZ- zP+52Iq}jh6v0?pBtNR1>y_H49AOHd&00JNY0w4eaAOHd&00Q|4 z@Q>B8?jS8qXuIwpQS4{2?%?XRkG#I`#=m?opD%!Q2lI79{67eQ00@8p2!H?xfB*=9 z00@9U9}(cb0M;F(6$)+F9i%6xpT)X^H#I&}H~6JD&g@T18F$jPF2!H?xfB*=900@8p2!H?x^f`fJd)-08-_K&*!Nm869RL2mPJg7_*shiG zbq6h19mKkWecpS~01yBH5CDNgAb@oT4_r4cdkLy(*~?#vb>|l4Tz8J*m4&0M<2!T4 zXD?G_9p7OWUqJl}?BbQBtgP#Avx!$1x3Z3JwToAlyRwd-Zx^pDfMp#Ywu@Jm#IlaZ zx`UeI1+i?}@Z$Z~(#yVj)V1`Uw~kQys~Yi~MMU-$qDDMx9br?)Gu9ExRiM`Kv}J_a z@70KH))7iiS0k|QVD<~Ira2HTl`oRqZ zKmY_l00ck)1V8`;KmY_l00cnbh$C<+b8459kwkf8sws7hSvh`UP1VH8+OgwmDyt?; zs2U?usnBUDzQC2GZMUx+J#;+x1+?Qd_yR}V^Nt3C00@8p2!H?xfB*=900@8p2!H@5 z&@*2ku>AE~NBm&X*D+rJy$A$A00ck)1V8`;KmY_l00ck)1de0^R=z;(#BsG_E34>M zInH{%fLZn$d-|)_UWxevNAh!zwu1l&fB*=900@8p2!H?xfB*=9K*7Gi37l1GeSyKB z-1y3u7hil+fBqo<9>GTI&Hd7$Z2aK3fdB}A00@8p2!H?xfB*=900@9UuMpsQ26V== z7>?4}Dw5$kTS+olXDdhs=xjOFyH#h)q+Bm$sgxyBu9LD@%C%Cuq+BCqk(7l}I!QL` zEKX9dvkockQWi*wNokW3mC`CDB4xgmc~ZhsT1aN;teKyfi>;@7(_QQ?k_%nz+a%|@ z*gBH&E_NqLrHkD`GTOy%CmHEtYe|N>*li?(TS5d-Sma^c9*B7uw+F%= zChP$Z5NVx;>GAXwJwzC!%H$K+lq#dN~r0t|J(gmbZ(imxkw2gEg zX_T~uw3W1pG(y@)I-j(jbROwk(lF_4*36oXz1o$mi8Ya4#zNw3d7V>5jgxqN;xRsT zV5)EkCJBR}R`>&cVGoQJ?!c+S95`8c1A~P%;1SM1nJ@Z2aK3fdB}A00@9UUlAD7?Jn1tgH?NrHLaKx>qebdruH3IQB`eo|kyP#!qVrM;guP!EiJliUy+%A^wI$@3M=p${)YTF1|8< zd}q%1+KS2o>hG|NFQEPfcJcoF?QgS*_fNK7agtyZE~N@nO68+Whg&cJa!^ zW8MEkcJa#PWF4RC|5`Q6>i;4yw_X3YxJI0GMGO#^-|*xWr4RI2l)JdCvm?~f7NHOI zD8Kg$))7kYRU@9aj!^om8u6S(MD`V;Mm%dBVN=I5))C58pw{uUWrW)A)rf7@5lT;2 zBc8I3Q2MqS@uYQx(wo(Yt=17r|5YQ-?q>8CL%j6Tt8iA=%lpzxo#qj{Xr=2M(D5lc zVi&DE#a7W{H8!UEB;_xj%c%3oj;_=#|KibQ!S)J7$-j@8(n?*K(!aDQo!wnCrSG*a zo#WUs>(WX$%^Z8GWofmGW=apVF0FLWbZKq3b!nw*W=e0dF0FLSOzCf0msYxDru0?T zrIi=ZOlf-lGw*eh&t>+@Wu`Q}-^us_Uz@o^|LF-Y-p73bX1uD=4{jg;0w4eaAOHd& z00JNY0w4eaAOHeK907}b1mh|vR@aQJ8CNx-y0Y3jPvN_z{}G9Q8d{Z_F92WQhH5dXQ#0>;M00ck)1V8`; zKmY_l00ck)1bT%4U!G5wc?xxzr%;!93U!&MP?vcMb(yD7mw5_xnWs>fc?xxzr%;!9 z3U!&MP?vcMb(yD7mw5_xnWs>fc?xy0ykNa9))yq<2?|Mgf(8r7vJgU%-^UfGK?e zQ~Cm?^aV`m3z*UuFr_bGN?*V$2k#zX4?OG<_P_>@um{$AggvmHz3 zfB*=900@8p2!H?xfB*=9z%fHWcWNTTAUj_S{Lc~N{_$eiU&T7bxc_a^Wdvk5>Gh;< zkuD|OMY@FaP15T~-ymH~`WorAq_2{8k?thDhV&KEMWip2E+pN~T3FopSc{W(khYVy zlg3CFkVZ*kq!H3K(s`s&(iYNI(k9XfDWy7L^GWMT=aJ4O4U^7h&8*qjt6j;OSQF`G zEF{jB*Evvhf;m0|5{K0T2KI5C8!X009sH0T2LzJ|)0E zei!b7_L+m*3)&|R?k{M2xWAyiBix0A@D>unSx5+9At79agzyv+!cj;FKOrI9goN-C z62eJH2p=IKW)j>jW)K9#1cHE=KM)Yp2LfXDKtN0$2#C1@0WozTAZ88(#KeJsm^Tn$ zVcuY;oG;*%^97u8zJOEC7jVk?0!}$!z$xboIOTi+r<^a~l=B6ga=w65%op&{+4@rR z1+JX^wX3G=`(1;~Q@C9-wwLd;+FZL2WuqE#0|5{K0T2LzJ|i%u+pT1C){QzZWpk#S zz83j{DW7lgd;4bq&Xl&u7tBjPAxoFgKUs06boKt};bp#HUV2UD*jlk(x+U3?YHEz> z_fHf*vNRP=>cJWFgZR_~3UA&T7+d974E?&8Ktm8v= z@yg|79iQs|S~bf`BA=a*`+#_vu3fnZDN(yVKwN&qi{%xi5A;}+Q*+tBU>%`cJ!-`B z))7j7RU@9Wh{(P|)QD%TBW&t;#yUc|3e-BDwv15wy&AF2Izs8`YQ$645lY`yBc8O5 zPAz~k+1-r(V)SFxXBE?{a8}o&mrkWy9nkeO&GCp`w9<8~q9bVl5X{AeMN?&DNT6qCYmLAhR z#Oj&Pe!0vXn@BDuQQe*A3>kJSw+Pl00JNY0w4eaAOHd&00JNY0>?Um zgUUy6&lj4H;HJ`lef)cqKDm#Z1y18#o{!)|aI9ZuIDZfT0T2KI5C8!X009sH0TAd*0?0?u zvwQ?1>K?JbTkH?WN3i7cf3!#2zTwU{U*J%(5xDpDW#2*FAOHd&00JNY0w4eaAOHd& z00JN&2=LF(kEMJB{Cu5qzJOEC7qG}jaMh;2pE~REFFnNb5jc&7l#jr8z*u--MuJ1h zM&R0w9tZ*;00JNY0)0sU`3NlY8Q3q2jeG>OdOh+H$VJ%?>_6GbJPueGUGyL1BPh|+ zIS-Tt*wyv#k&j^i{1DW)lttfFR>((CRh`aUbHHUsxe$<#;4sfeFs6H0j+_Bj`3S~z z56w~f3&}@t0)J2?<#Md>elB*vx`QRtT}`860|WVdfs{{xbq5c_ zD*#Ob0T2KI5C8!X009sH0T2LzW19f?1+ea5&UFW=bGT{UL6UwJ>kghW`OHl#E4E*f z_66RjRR^)|;IVy0;p{;G1V8`;KmY_l00ck)1VG@hBOunyTdisAe71`A=dhKuAJ10M zzKV6S<<#J}NtcoCCcU2YEz+f=yGWOizDar==^LbrNnazqmh@H9F4CQ(*O0zKx`^~; z(uJhkSqqCBA8T>a4$^kgcG4K>0@5gHj5I>pMmmo)O4>r&O4>vkA#Eg`Pg+kpk900+ zm~=L4X3fT4?Ml|fnn*8WA#t|6&Z(ltNxVMs*m`QuRCX8bC$VqSzLu?{y`SAl`_b$U z+Mmj9r~S!nE$s)h+eFko^zSlZ49NKcr_lZM_Xy^%Y&~WDt=~AAuP-3JNAQ?7g8pUF zulT+JDQ+MD0w4eaAOHd&00JNY0w4eaAOHe=N8l1?iRN{%YQ|0T2KI5C8!X009sH0T2KI5C8#{ z0QUua+!r{5Kc4ah9(@0_1$W*2V0qdX*r7(@9teN{2!H?xfB*=900@8p2!H?xfWTo- z;0)(XR><-K+!yfi-;^)#k6mpamaLdPGrjKMLW8#Qo!Ogf_n~ZuJwG%M1V8`;K;Wn* zFs9pGt}$Ax$6Ksv#jIF2>b!EPo^ksx)gzbjaZr)>tV`EemwrN)F3CE!vJ6n>*lRM! z){6DgEyWx~z4ErnQ74 zjpp=VI2sQ{gVBZ%>(n&8%PziZ|I@LKUt||wnLoZWXMAl1t%$VWI!V^`ci6=jQ2zqE zcz^!-+ic?f6Y|Hm+QpC0A3xtNzAk@!*e<>{e|)oDymIkaKYt;+c;#}kj!*S}t(s-^ z|Lk?P4v4q7M(Ay@K0sW4!;@E(KG0)P?&7x2j!;Wmgcc3gG{+0p5lZh>Bc8X8Q2MJH z@tj3O_7$Q=JZl|cQ^zyb5z1Ad*73Augxc@bh;7ypN>5iKp0bWm`nDSJq;-VSo7IS| z))7koRU^*sX7m?By!6tma8}pL`_fCD<`KJSrRyBfaVa`t7p*+SR?%ZMHm3U|eY9DAx|X|;=H zN)NOyt#r?HX>GT4X{BpsN^h|)t#r#|=`r2ItezcuNmIIH=Ga$RA6t0=&6K9+Kl5HE z`CMkdTxLp(sR1cp;Iwns%`S>O|46yhd4P9009sH0T2KI z5C8!X009sH0T4Kx2zYhpI7g^yPCV2an-gdcMmvHH@o-zTgDoGRJ5O^c58YG7oWt0Z zhK9DzXuQK^e54hXx#*D@!AP(@9O@`?JDelfv|v2c+{W)$(V3Q)q{`DBB~RhM-+cdH zPWjP|YxsNtW;~|R5AqZqPJIJy0Ra#I0T2KI5C8!X009sH0T4LW30zW?ou^Pgq&$U6 zK7vQ?yqjJ7+bfUblL(lxSEC=?KmY_l00ck)1V8`;KmY_l00ck)1dciaA|F8!otBc1 z;NiwSFK+7a!528{o^PBG2!H?xfB*=900@8p2!H?xfB*>WPe9}&;JyIQN0453aL}^m zPubNC|4+^r*r6H7N3cIM{2K&700ck)1V8`;KmY_l00ck)1dap(h2|sRzJRjs;Gh%! z%Qx-rIaOG9@JM)?(MAve0T2KI5C8!X009sH0T2LzqmsZutvh)Cp{_f40%w@YM{wf@ zC!BI)@u1teS>QC@)r?P#4~=(?joF*~r9;_{%4ZEH1p*)d0w4eaAOHd&00JNY0w8dB z5O6t}LuadL-MmpcTSYQlXDdkt>ud$d0G%zThHll_GAY+fSt@0TlE-BYY zStMnl6iN!LZPr(Y_ts@!lVt0~My4W2gqh0KFl94X9mSm`l-6q5DmvWz!d+Gm7`c3-+ zf2;V{)8-AjbZ9 zzJOQy0$%A0n9>(8r7vJI;R~2d_yQ&qzJSSuFJLm^3z$s!0wxo_fXRd}U^3wgm`wNr zCKJAZR}S7i!X9|oBkX|<9>(o~^&ZCUfwdmS?SYjZ#_fR{JdE1|OFfL+1B*P2+XFEV zIiRKKy@`l#l;+l5zv!6;kd1yiCe1fbFb>#f^`(IB5rIJ83&< zjC28Mlr%;fA#Ed_M;awDj$$_{uXYhq2Lm$8sI zTVCf>QR5_DpLmQ<9hfQ{f=R+4s1^QzU)Teqg*$MnFb7T+-oRjXn~1t!?C%qM?hA~h z-?T3^973Je1Re{U%*Fao6bitYR%0r zZG0qpVmUqNG!|;ccH;qKVfoJN&9(bbHaa+NAOHd&00JP;X9UJ{yUR7^VAbAYO)F-_ zx>4t?@>kVX_{UXLRhxeQMDa5v5{U)798}~z>(VvWrJswU4>t%U|LH!(r8W(hNJONG#G6N z@i!!TmtB0-{tdK_Ut||wnLoZWXMAl%WdZef*u@u6{{p*sfByEj*~I%NImKKOwt#<#Mu)PxXJTnq~EWx_IsTzr{7;q$^^8xcr7E zuPA+>$D-WDZJiyVmbM6esHbU;7pxM%oXRITX zt3a*eY0C(;->VVZtRs}3u0}j%9ijAXHR4I@2&Ffx5nHVzl>VzmoZZdnFNS#OrB~ss zu9x?vmpaWOcF{`LIiTZHbi^)Nd5W!~$7*a$_esiMJeN`DlO0{DTmHqP%Yy9{h?0LF zGo_WfGNpfMQ98T3W=h{{T{_3HW7egWZkjpvRLjz87tNF&XkA+Ap6SxsZtK!Y*UXgO zVqIG4mdVm%y3MR-N9WIu_4|>cG2O#+lvZ9q<*}*s$vH|ZFPF*E^p=cI4M@)y_{UG) z`PKu+E#HWI1by~q6%~U32!H?xfB*=900@8p2!H?x9P0!QDj&gyFEk&)1DCz}L+2-V z)N-?+$apK2kKnD;I)f=alxzgv4>o-^;#}zqyq(HN@HX-h9P5`A z&L0Fo00ck)1V8`;KmY_l00a&v0%9%w)wC)C@(~o6k3bOHAojNT2(+J!_~!AE&*w{D z;E$T|N9*|lhmwuJz3*`9OK1xSfB*=900@8p2!H?xfB*=9z%fdIe~f-?)#_EfqaFMtmxWO7JXM)As>O=Wk=Zz zkdHupX-?-1V7;1;;PmbhlnH>b>Onl~kyhebe40!$prjfKggWAfRXzeGPvH;GzU;M+ zcMg6GzQ9rWMi?gr0w4eaARq|97l1F2ootNKYALC&tjD#;Q)uIhD0$wj`N0=}FF>Pw uB@vuD%155U>;$vOQ<%vpoXnV*{bHRN=^sXU3T=FW$_eQd#D|wJ@c#iDR>$Z7 literal 1796352 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 diff --git a/BudgetApp/wwwroot/js/chart-interop.js b/BudgetApp/wwwroot/js/chart-interop.js index b733338..4418a3b 100644 --- a/BudgetApp/wwwroot/js/chart-interop.js +++ b/BudgetApp/wwwroot/js/chart-interop.js @@ -2,7 +2,7 @@ window.chartInterop = { chartInstances: {}, transactionLabels: {}, - createOrUpdateChart: function (canvasId, labels, datasets, dayTransactionLabels, highlightIndex, currencySymbol) { + createOrUpdateChart: function (canvasId, labels, datasets, dayTransactionLabels, highlightIndex, salaryDayIndices, currencySymbol) { const canvas = document.getElementById(canvasId); if (!canvas) return; @@ -88,6 +88,32 @@ window.chartInterop = { }; const storedLabels = this.transactionLabels[canvasId]; + const salaryIndices = salaryDayIndices || []; + + // Salary payday highlight plugin — subtle green band + 💰 marker + const salaryPlugin = { + id: 'salaryDays', + afterDraw: (chart) => { + const indices = chart.config._salaryDayIndices; + if (!indices || indices.length === 0) return; + const meta = chart.getDatasetMeta(0); + if (!meta) return; + const ctx = chart.ctx; + const top = chart.chartArea.top; + const bottom = chart.chartArea.bottom; + indices.forEach(idx => { + if (!meta.data[idx]) return; + const x = meta.data[idx].x; + ctx.save(); + ctx.fillStyle = 'rgba(25, 135, 84, 0.10)'; + ctx.fillRect(x - 6, top, 12, bottom - top); + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('💰', x, top + 14); + ctx.restore(); + }); + } + }; const chartInstance = new Chart(canvas, { type: 'line', @@ -140,11 +166,12 @@ window.chartInterop = { } } }, - plugins: [highlightPlugin, crosshairPlugin] + plugins: [highlightPlugin, crosshairPlugin, salaryPlugin] }); chartInstance.config._highlightIndex = (highlightIndex != null && highlightIndex >= 0) ? highlightIndex : null; - if (highlightIndex != null && highlightIndex >= 0) { + chartInstance.config._salaryDayIndices = salaryIndices; + if ((highlightIndex != null && highlightIndex >= 0) || salaryIndices.length > 0) { chartInstance.update(); }