diff --git a/FinTrack/App.xaml.cs b/FinTrack/App.xaml.cs index 07fb0cd..01975c7 100644 --- a/FinTrack/App.xaml.cs +++ b/FinTrack/App.xaml.cs @@ -1,7 +1,13 @@ using CommunityToolkit.Mvvm.Messaging; using FinTrackForWindows.Core; using FinTrackForWindows.Services; +using FinTrackForWindows.Services.Accounts; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.Budgets; +using FinTrackForWindows.Services.Currencies; +using FinTrackForWindows.Services.Debts; +using FinTrackForWindows.Services.Memberships; +using FinTrackForWindows.Services.Transactions; using FinTrackForWindows.ViewModels; using FinTrackForWindows.Views; using Microsoft.Extensions.DependencyInjection; @@ -74,6 +80,13 @@ private void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } protected override async void OnStartup(StartupEventArgs e) diff --git a/FinTrack/Data/AppDatabaseContext.cs b/FinTrack/Data/AppDatabaseContext.cs index 6de625b..26de090 100644 --- a/FinTrack/Data/AppDatabaseContext.cs +++ b/FinTrack/Data/AppDatabaseContext.cs @@ -11,7 +11,7 @@ public AppDatabaseContext() { } public DbSet Users { get; set; } public DbSet UserSettings { get; set; } public DbSet Accounts { get; set; } - public DbSet Transactions { get; set; } + public DbSet Transactions { get; set; } public DbSet Categories { get; set; } public DbSet Budgets { get; set; } public DbSet BudgetCategories { get; set; } @@ -85,7 +85,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasIndex(a => new { a.UserId, a.Name }).IsUnique(); }); - modelBuilder.Entity(entity => + modelBuilder.Entity(entity => { entity.ToTable("Transactions"); entity.HasKey(t => t.Id); diff --git a/FinTrack/Dtos/CurrencyDtos/ConvertResponseDto.cs b/FinTrack/Dtos/CurrencyDtos/ConvertResponseDto.cs new file mode 100644 index 0000000..923968d --- /dev/null +++ b/FinTrack/Dtos/CurrencyDtos/ConvertResponseDto.cs @@ -0,0 +1,12 @@ +namespace FinTrackForWindows.Dtos.CurrencyDtos +{ + public class ConvertResponseDto + { + public string From { get; set; } = string.Empty; + public string To { get; set; } = string.Empty; + public decimal Amount { get; set; } + public decimal Result { get; set; } + public decimal Rate { get; set; } + public DateTime Timestamp { get; set; } + } +} diff --git a/FinTrack/Dtos/MembershipDtos/CheckoutResponseDto.cs b/FinTrack/Dtos/MembershipDtos/CheckoutResponseDto.cs new file mode 100644 index 0000000..29ef684 --- /dev/null +++ b/FinTrack/Dtos/MembershipDtos/CheckoutResponseDto.cs @@ -0,0 +1,7 @@ +namespace FinTrackForWindows.Dtos.MembershipDtos +{ + public class CheckoutResponseDto + { + public string CheckoutUrl { get; set; } = string.Empty; + } +} diff --git a/FinTrack/Dtos/MembershipDtos/PlanFeatureDto.cs b/FinTrack/Dtos/MembershipDtos/PlanFeatureDto.cs new file mode 100644 index 0000000..2abc1ec --- /dev/null +++ b/FinTrack/Dtos/MembershipDtos/PlanFeatureDto.cs @@ -0,0 +1,51 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.MembershipDtos +{ + public class PlanFeatureDto + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public decimal Price { get; set; } + public BaseCurrencyType? Currency { get; set; } + public BillingCycleType? BillingCycle { get; set; } + public int? DurationInDays { get; set; } + public ReportingFeatures? Reporting { get; set; } + public EmailingFeatures? Emailing { get; set; } + public BudgetingFeatures? Budgeting { get; set; } + public AccountFeatures? Accounts { get; set; } + public bool IsActive { get; set; } = true; + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public bool PrioritySupport { get; set; } = false; + } + + public class ReportingFeatures + { + public string Level { get; set; } = "Basic"; + public bool CanExportPdf { get; set; } = false; + public bool CanExportWord { get; set; } = false; + public bool CanExportMarkdown { get; set; } = false; + public bool CanExportXml { get; set; } = false; + public bool CanExportText { get; set; } = false; + public bool CanExportXlsx { get; set; } = false; + } + + public class EmailingFeatures + { + public bool CanEmailReports { get; set; } = false; + public int MaxEmailsPerMonth { get; set; } = 0; + } + + public class BudgetingFeatures + { + public bool CanCreateBudgets { get; set; } = false; + public int MaxBudgets { get; set; } = 0; + } + + public class AccountFeatures + { + public int MaxBankAccounts { get; set; } = 0; + } +} diff --git a/FinTrack/Dtos/MembershipDtos/SubscriptionRequestDto.cs b/FinTrack/Dtos/MembershipDtos/SubscriptionRequestDto.cs new file mode 100644 index 0000000..cc10629 --- /dev/null +++ b/FinTrack/Dtos/MembershipDtos/SubscriptionRequestDto.cs @@ -0,0 +1,8 @@ +namespace FinTrackForWindows.Dtos.MembershipDtos +{ + public class SubscriptionRequestDto + { + public int PlanId { get; set; } + public bool AutoRenew { get; set; } + } +} diff --git a/FinTrack/Dtos/MembershipDtos/UserMembershipDto.cs b/FinTrack/Dtos/MembershipDtos/UserMembershipDto.cs new file mode 100644 index 0000000..b12e4ac --- /dev/null +++ b/FinTrack/Dtos/MembershipDtos/UserMembershipDto.cs @@ -0,0 +1,13 @@ +namespace FinTrackForWindows.Dtos.MembershipDtos +{ + public class UserMembershipDto + { + public int Id { get; set; } + public int PlanId { get; set; } + public string PlanName { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Status { get; set; } = string.Empty; + public bool AutoRenew { get; set; } + } +} diff --git a/FinTrack/Dtos/TransactionDtos/TransactionCategoryCreateDto.cs b/FinTrack/Dtos/TransactionDtos/TransactionCategoryCreateDto.cs new file mode 100644 index 0000000..1814c8c --- /dev/null +++ b/FinTrack/Dtos/TransactionDtos/TransactionCategoryCreateDto.cs @@ -0,0 +1,10 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.TransactionDtos +{ + public class TransactionCategoryCreateDto + { + public string Name { get; set; } = null!; + public TransactionType Type { get; set; } + } +} diff --git a/FinTrack/Dtos/TransactionDtos/TransactionCategoriesDto.cs b/FinTrack/Dtos/TransactionDtos/TransactionCategoryDto.cs similarity index 65% rename from FinTrack/Dtos/TransactionDtos/TransactionCategoriesDto.cs rename to FinTrack/Dtos/TransactionDtos/TransactionCategoryDto.cs index 9d7f1e1..10cb4d4 100644 --- a/FinTrack/Dtos/TransactionDtos/TransactionCategoriesDto.cs +++ b/FinTrack/Dtos/TransactionDtos/TransactionCategoryDto.cs @@ -1,12 +1,17 @@ using FinTrackForWindows.Enums; +using System.Text.Json.Serialization; namespace FinTrackForWindows.Dtos.TransactionDtos { - public class TransactionCategoriesDto + public class TransactionCategoryDto { public int Id { get; set; } public string Name { get; set; } = null!; + public int UserId { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] public TransactionType Type { get; set; } + public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } } diff --git a/FinTrack/Dtos/TransactionDtos/TransactionCategoryUpdateDto.cs b/FinTrack/Dtos/TransactionDtos/TransactionCategoryUpdateDto.cs new file mode 100644 index 0000000..56afc1f --- /dev/null +++ b/FinTrack/Dtos/TransactionDtos/TransactionCategoryUpdateDto.cs @@ -0,0 +1,10 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.TransactionDtos +{ + public class TransactionCategoryUpdateDto + { + public string Name { get; set; } = null!; + public TransactionType Type { get; set; } + } +} diff --git a/FinTrack/Dtos/TransactionDtos/TransactionDto.cs b/FinTrack/Dtos/TransactionDtos/TransactionDto.cs index 2cad418..b03c1d5 100644 --- a/FinTrack/Dtos/TransactionDtos/TransactionDto.cs +++ b/FinTrack/Dtos/TransactionDtos/TransactionDto.cs @@ -6,7 +6,7 @@ namespace FinTrackForWindows.Dtos.TransactionDtos public class TransactionDto { public int Id { get; set; } - public TransactionCategoriesDto Category { get; set; } = null!; + public TransactionCategoryDto Category { get; set; } = null!; public AccountDto Account { get; set; } = null!; public decimal Amount { get; set; } public BaseCurrencyType Currency { get; set; } diff --git a/FinTrack/Enums/BillingCycleType.cs b/FinTrack/Enums/BillingCycleType.cs new file mode 100644 index 0000000..2d3b966 --- /dev/null +++ b/FinTrack/Enums/BillingCycleType.cs @@ -0,0 +1,14 @@ +namespace FinTrackForWindows.Enums +{ + public enum BillingCycleType + { + None, + Monthly, + Quarterly, + SemiAnnually, + Annually, + Biennially, + Triennially, + Custom + } +} diff --git a/FinTrack/Helpers/EqualityToBooleanConverter.cs b/FinTrack/Helpers/EqualityToBooleanConverter.cs new file mode 100644 index 0000000..a425724 --- /dev/null +++ b/FinTrack/Helpers/EqualityToBooleanConverter.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class EqualityToBooleanConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values == null || values.Length < 2 || values[0] == null || values[1] == null) + return false; + + return values[0].Equals(values[1]); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/FinTrack/Helpers/InequalityToBooleanConverter.cs b/FinTrack/Helpers/InequalityToBooleanConverter.cs new file mode 100644 index 0000000..5248a3b --- /dev/null +++ b/FinTrack/Helpers/InequalityToBooleanConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class InequalityToBooleanConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values == null || values.Length < 2 || values[0] == null || values[1] == null) + { + return true; + } + + return !values[0].Equals(values[1]); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/FinTrack/Helpers/NullToVisibilityInverseConverter.cs b/FinTrack/Helpers/NullToVisibilityInverseConverter.cs new file mode 100644 index 0000000..1bb816b --- /dev/null +++ b/FinTrack/Helpers/NullToVisibilityInverseConverter.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class NullToVisibilityInverseConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value == null ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/FinTrack/Models/Account/AccountModel.cs b/FinTrack/Models/Account/AccountModel.cs index e104504..5b9b9fc 100644 --- a/FinTrack/Models/Account/AccountModel.cs +++ b/FinTrack/Models/Account/AccountModel.cs @@ -21,7 +21,8 @@ public partial class AccountModel : ObservableObject [ObservableProperty] private List history = new(); - public decimal? balance { get; set; } + [ObservableProperty] + private decimal? balance; public string IconPath => Type switch { diff --git a/FinTrack/Models/AccountModel.cs b/FinTrack/Models/AccountModel.cs index 49efeb2..1c004ab 100644 --- a/FinTrack/Models/AccountModel.cs +++ b/FinTrack/Models/AccountModel.cs @@ -47,6 +47,6 @@ public class AccountModel [Column("IsSynced")] public bool IsSynced { get; set; } = false; - public virtual ICollection Transactions { get; set; } = new List(); + public virtual ICollection Transactions { get; set; } = new List(); } } diff --git a/FinTrack/Models/Budget/BudgetModel.cs b/FinTrack/Models/Budget/BudgetModel.cs index 3564f50..555542d 100644 --- a/FinTrack/Models/Budget/BudgetModel.cs +++ b/FinTrack/Models/Budget/BudgetModel.cs @@ -1,6 +1,4 @@ -// FinTrackForWindows.Models.Budget/BudgetModel.cs - -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using FinTrackForWindows.Enums; using System.Globalization; diff --git a/FinTrack/Models/CategoryModel.cs b/FinTrack/Models/CategoryModel.cs index 324f91a..6272155 100644 --- a/FinTrack/Models/CategoryModel.cs +++ b/FinTrack/Models/CategoryModel.cs @@ -29,7 +29,7 @@ public class CategoryModel public bool IsSynced { get; set; } = false; public virtual ICollection BudgetAllocations { get; set; } = new List(); - public virtual ICollection Transactions { get; set; } = new List(); + public virtual ICollection Transactions { get; set; } = new List(); } public enum CategoryType diff --git a/FinTrack/Models/Membership/MembershipModel.cs b/FinTrack/Models/Membership/MembershipModel.cs new file mode 100644 index 0000000..f9ea7ba --- /dev/null +++ b/FinTrack/Models/Membership/MembershipModel.cs @@ -0,0 +1,6 @@ +namespace FinTrackForWindows.Models.Membership +{ + public class MembershipModel + { + } +} diff --git a/FinTrack/Models/Transaction/AddCategoryViewModel.cs b/FinTrack/Models/Transaction/AddCategoryViewModel.cs new file mode 100644 index 0000000..209bdbf --- /dev/null +++ b/FinTrack/Models/Transaction/AddCategoryViewModel.cs @@ -0,0 +1,56 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.TransactionDtos; +using FinTrackForWindows.Enums; +using FinTrackForWindows.Services.Api; +using System.Windows; + +namespace FinTrackForWindows.Models.Transaction +{ + public partial class AddCategoryViewModel : ObservableObject + { + private readonly IApiService _apiService; + + public Action? CloseWindow { get; set; } + public TransactionCategoryModel? CreatedCategory { get; private set; } + + [ObservableProperty] + private string name = string.Empty; + + [ObservableProperty] + private TransactionType type = TransactionType.Expense; + + public AddCategoryViewModel(IApiService apiService) + { + _apiService = apiService; + } + + [RelayCommand] + private async Task Save() + { + if (string.IsNullOrWhiteSpace(Name)) + { + MessageBox.Show("Kategori adı boş olamaz.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + var createDto = new TransactionCategoryDto + { + Name = this.Name, + Type = this.Type + }; + + var result = await _apiService.PostAsync("TransactionCategory", createDto); + + if (result != null) + { + CreatedCategory = new TransactionCategoryModel { Id = result.Id, Name = result.Name, Type = result.Type }; + CloseWindow?.Invoke(); + } + else + { + MessageBox.Show("Kategori oluşturulurken bir hata oluştu.", "API Hatası", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } +} diff --git a/FinTrack/Models/Transaction/TransactionCategoryModel.cs b/FinTrack/Models/Transaction/TransactionCategoryModel.cs new file mode 100644 index 0000000..4ee5a27 --- /dev/null +++ b/FinTrack/Models/Transaction/TransactionCategoryModel.cs @@ -0,0 +1,17 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Models.Transaction +{ + public partial class TransactionCategoryModel : ObservableObject + { + [ObservableProperty] + private int id; + + [ObservableProperty] + private string name = string.Empty; + + [ObservableProperty] + private TransactionType type; + } +} diff --git a/FinTrack/Models/Transaction/TransactionModel.cs b/FinTrack/Models/Transaction/TransactionModel.cs index 24a8884..0689cb7 100644 --- a/FinTrack/Models/Transaction/TransactionModel.cs +++ b/FinTrack/Models/Transaction/TransactionModel.cs @@ -18,22 +18,28 @@ public partial class TransactionModel : ObservableObject [ObservableProperty] private DateTime date = DateTime.Now; + [ObservableProperty] + private int accountId; + [ObservableProperty] private string accountName = string.Empty; [ObservableProperty] - private string category = string.Empty; + private int categoryId; + + [ObservableProperty] + private string categoryName = string.Empty; [ObservableProperty] - private string currency = "USD"; + private BaseCurrencyType currency; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IconBackground))] [NotifyPropertyChangedFor(nameof(AmountForegroundColor))] private TransactionType type; - private static readonly Brush IncomeBrush = new SolidColorBrush(Colors.Green); - private static readonly Brush ExpenseBrush = new SolidColorBrush(Colors.Red); + private static readonly Brush IncomeBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); + private static readonly Brush ExpenseBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68)); private static readonly Brush DefaultBrush = new SolidColorBrush(Colors.Gray); public Brush IconBackground => Type switch @@ -50,4 +56,4 @@ public partial class TransactionModel : ObservableObject _ => DefaultBrush }; } -} +} \ No newline at end of file diff --git a/FinTrack/Models/TransactionModel.cs b/FinTrack/Models/TransactionModel.cs index ae4a6a1..4c141c6 100644 --- a/FinTrack/Models/TransactionModel.cs +++ b/FinTrack/Models/TransactionModel.cs @@ -4,7 +4,7 @@ namespace FinTrackForWindows.Models { [Table("Transactions")] - public class TransactionModel + public class TransactionModel_ { [Key] [Required] diff --git a/FinTrack/Models/UserModel.cs b/FinTrack/Models/UserModel.cs index 0754842..6f41b4d 100644 --- a/FinTrack/Models/UserModel.cs +++ b/FinTrack/Models/UserModel.cs @@ -76,7 +76,7 @@ public class UserModel public virtual UserSettingsModel? UserSettings { get; set; } public virtual ICollection Accounts { get; set; } = new List(); - public virtual ICollection Transactions { get; set; } = new List(); + public virtual ICollection Transactions { get; set; } = new List(); public virtual ICollection Categories { get; set; } = new List(); public virtual ICollection Budgets { get; set; } = new List(); public virtual ICollection Notifications { get; set; } = new List(); diff --git a/FinTrack/Services/Accounts/AccountStore.cs b/FinTrack/Services/Accounts/AccountStore.cs new file mode 100644 index 0000000..dec819e --- /dev/null +++ b/FinTrack/Services/Accounts/AccountStore.cs @@ -0,0 +1,112 @@ +using FinTrackForWindows.Dtos.AccountDtos; +using FinTrackForWindows.Models.Account; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace FinTrackForWindows.Services.Accounts +{ + public class AccountStore : IAccountStore + { + private readonly IApiService _apiService; + + private readonly ILogger _logger; + + private readonly ObservableCollection _accounts; + + public ReadOnlyObservableCollection Accounts { get; } + + public event NotifyCollectionChangedEventHandler? AccountsChanged; + + public AccountStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + _accounts = new ObservableCollection(); + Accounts = new ReadOnlyObservableCollection(_accounts); + + _accounts.CollectionChanged += OnInternalCollectionChanged; + } + + public async Task LoadAccountsAsync() + { + if (_accounts.Any()) + { + _logger.LogInformation("Hesaplar zaten yüklü. API çağrısı atlanıyor."); + return; + } + + try + { + var AccountsFromApi = await _apiService.GetAsync>("Account"); + if (AccountsFromApi == null) return; + + _accounts.Clear(); + foreach (var dto in AccountsFromApi) + { + _accounts.Add(new AccountModel + { + Id = dto.Id, + Name = dto.Name, + Type = dto.Type, + Currency = dto.Currency, + Balance = dto.Balance, + }); + } + _logger.LogInformation("{Count} adet hesap Accountstore'a yüklendi.", _accounts.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Accountstore'da hesaplar yüklenirken hata oluştu."); + } + } + + public async Task AddAccountAsync(AccountCreateDto newAccountDto) + { + var createdAccountDto = await _apiService.PostAsync("Account", newAccountDto); + if (createdAccountDto != null) + { + _accounts.Add(new AccountModel + { + Id = createdAccountDto.Id, + Name = createdAccountDto.Name, + Currency = createdAccountDto.Currency, + Type = createdAccountDto.Type, + }); + } + } + + public async Task DeleteAccountAsync(int accountId) + { + await _apiService.DeleteAsync($"Account/{accountId}"); + var accountToRemove = _accounts.FirstOrDefault(b => b.Id == accountId); + if (accountToRemove != null) + { + _accounts.Remove(accountToRemove); + } + } + + public async Task UpdateAccountAsync(int accountId, AccountCreateDto updatedAccount) + { + var updateAccountDto = await _apiService.PutAsync($"Account/{accountId}", updatedAccount); + if (updatedAccount != null) + { + foreach (var item in _accounts) + { + if (item.Id == accountId) + { + item.Name = updatedAccount.Name; + item.Currency = updatedAccount.Currency; + item.Type = updatedAccount.Type; + } + } + } + } + + private void OnInternalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + AccountsChanged?.Invoke(this, e); + } + } +} diff --git a/FinTrack/Services/Accounts/IAccountStore.cs b/FinTrack/Services/Accounts/IAccountStore.cs new file mode 100644 index 0000000..acbe1e4 --- /dev/null +++ b/FinTrack/Services/Accounts/IAccountStore.cs @@ -0,0 +1,20 @@ +using FinTrackForWindows.Dtos.AccountDtos; +using FinTrackForWindows.Models.Account; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace FinTrackForWindows.Services.Accounts +{ + public interface IAccountStore + { + ReadOnlyObservableCollection Accounts { get; } + + event NotifyCollectionChangedEventHandler? AccountsChanged; + + Task LoadAccountsAsync(); + + Task AddAccountAsync(AccountCreateDto newBudget); + Task UpdateAccountAsync(int accountId, AccountCreateDto updatedBudget); + Task DeleteAccountAsync(int accountId); + } +} diff --git a/FinTrack/Services/Api/ApiService.cs b/FinTrack/Services/Api/ApiService.cs index b3741d6..f8f9333 100644 --- a/FinTrack/Services/Api/ApiService.cs +++ b/FinTrack/Services/Api/ApiService.cs @@ -25,7 +25,7 @@ public ApiService(ILogger logger, IConfiguration configuration) _logger = logger; _configuration = configuration; - _baseUrl = "http://localhost:5246/"; + _baseUrl = "http://localhost:5000/"; //_baseUrl = _configuration["BaseServerUrl"]; _httpClient = new HttpClient { @@ -233,6 +233,50 @@ private void AddAuthorizationHeader() } } + public async Task CreateCategoryAsync(string endpoint, object payload) + { + _logger.LogInformation("Kategori oluşturma (POST) isteği başlatılıyor: {Endpoint}", endpoint); + if (string.IsNullOrWhiteSpace(endpoint)) + { + _logger.LogError("Kategori oluşturma isteği sırasında endpoint boş veya null."); + throw new ArgumentException("Endpoint cannot be null or empty", nameof(endpoint)); + } + if (payload == null) + { + _logger.LogError("Kategori oluşturma isteği sırasında veri (payload) null."); + throw new ArgumentNullException(nameof(payload)); + } + + try + { + AddAuthorizationHeader(); + + var response = await _httpClient.PostAsJsonAsync(endpoint, payload, _jsonSerializerOptions); + + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(_jsonSerializerOptions); + + _logger.LogInformation("Kategori oluşturma (POST) isteği başarılı: {Endpoint}", endpoint); + return result; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Kategori oluşturma sırasında HTTP hatası oluştu: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); + return false; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Kategori oluşturma sırasında JSON deserialize hatası oluştu. API'nin 'true'/'false' döndüğünden emin olun.", endpoint); + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Kategori oluşturma sırasında genel bir hata oluştu: {Endpoint}", endpoint); + return false; + } + } + public async Task UploadFileAsync(string endpoint, string filePath) { _logger.LogInformation("Dosya yükleme isteği başlatılıyor: {Endpoint}, Dosya: {FilePath}", endpoint, filePath); diff --git a/FinTrack/Services/Api/IApiService.cs b/FinTrack/Services/Api/IApiService.cs index 5a91a5e..fd8470f 100644 --- a/FinTrack/Services/Api/IApiService.cs +++ b/FinTrack/Services/Api/IApiService.cs @@ -6,6 +6,7 @@ public interface IApiService Task PostAsync(string endpoint, object data); Task PutAsync(string endpoint, object data); Task DeleteAsync(string endpoint); + Task CreateCategoryAsync(string endpoint, object payload); Task UploadFileAsync(string endpoint, string filePath); Task<(byte[] FileBytes, string FileName)?> PostAndDownloadReportAsync(string endpoint, T payload); } diff --git a/FinTrack/Services/AuthService.cs b/FinTrack/Services/AuthService.cs index 3edee7e..550af0d 100644 --- a/FinTrack/Services/AuthService.cs +++ b/FinTrack/Services/AuthService.cs @@ -16,7 +16,7 @@ public AuthService(IConfiguration configuration) _configuration = configuration; _httpClient = new HttpClient { - BaseAddress = new Uri("http://localhost:5246/") + BaseAddress = new Uri("http://localhost:5000/") //BaseAddress = new Uri(_configuration["BaseServerUrl"]) }; diff --git a/FinTrack/Services/Budgets/BudgetService.cs b/FinTrack/Services/Budgets/BudgetService.cs deleted file mode 100644 index 1720a8e..0000000 --- a/FinTrack/Services/Budgets/BudgetService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace FinTrack.Services.Budgets -{ - public class BudgetService : IBudgetService - { - } -} diff --git a/FinTrack/Services/Budgets/BudgetStore.cs b/FinTrack/Services/Budgets/BudgetStore.cs new file mode 100644 index 0000000..78ef27a --- /dev/null +++ b/FinTrack/Services/Budgets/BudgetStore.cs @@ -0,0 +1,124 @@ +using FinTrackForWindows.Dtos.BudgetDtos; +using FinTrackForWindows.Models.Budget; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace FinTrackForWindows.Services.Budgets +{ + public class BudgetStore : IBudgetStore + { + private readonly IApiService _apiService; + + private readonly ILogger _logger; + + private readonly ObservableCollection _budgets; + + public ReadOnlyObservableCollection Budgets { get; } + + public event NotifyCollectionChangedEventHandler? BudgetsChanged; + + public BudgetStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + _budgets = new ObservableCollection(); + Budgets = new ReadOnlyObservableCollection(_budgets); + + _budgets.CollectionChanged += OnInternalCollectionChanged; + } + + public async Task LoadBudgetsAsync() + { + if (_budgets.Any()) + { + _logger.LogInformation("Bütçeler zaten yüklü. API çağrısı atlanıyor."); + return; + } + + try + { + var budgetsFromApi = await _apiService.GetAsync>("Budgets"); + if (budgetsFromApi == null) return; + + _budgets.Clear(); + foreach (var dto in budgetsFromApi) + { + _budgets.Add(new BudgetModel + { + Id = dto.Id, + Name = dto.Name, + Description = dto.Description, + Category = dto.Category, + AllocatedAmount = dto.AllocatedAmount, + Currency = dto.Currency, + StartDate = dto.StartDate, + EndDate = dto.EndDate, + }); + } + _logger.LogInformation("{Count} adet bütçe BudgetStore'a yüklendi.", _budgets.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "BudgetStore'da bütçeler yüklenirken hata oluştu."); + } + + } + + public async Task AddBudgetAsync(BudgetCreateDto newBudgetDto) + { + var createdBudgetDto = await _apiService.PostAsync("Budgets", newBudgetDto); + if (createdBudgetDto != null) + { + _budgets.Add(new BudgetModel + { + Id = createdBudgetDto.Id, + Name = createdBudgetDto.Name, + Description = createdBudgetDto.Description, + Category = createdBudgetDto.Category, + AllocatedAmount = createdBudgetDto.AllocatedAmount, + Currency = createdBudgetDto.Currency, + StartDate = createdBudgetDto.StartDate, + EndDate = createdBudgetDto.EndDate + }); + } + } + + public async Task DeleteBudgetAsync(int budgetId) + { + await _apiService.DeleteAsync($"Budgets/{budgetId}"); + var budgetToRemove = _budgets.FirstOrDefault(b => b.Id == budgetId); + if (budgetToRemove != null) + { + _budgets.Remove(budgetToRemove); + } + } + + public async Task UpdateBudgetAsync(int budgetId, BudgetCreateDto updatedBudget) + { + var updateBudgetDto = await _apiService.PutAsync($"Budgets/{budgetId}", updatedBudget); + if (updateBudgetDto != null) + { + foreach (var item in _budgets) + { + if (item.Id == budgetId) + { + item.Name = updatedBudget.Name; + item.Description = updatedBudget.Description; + item.Category = updatedBudget.Category; + item.AllocatedAmount = updatedBudget.AllocatedAmount; + item.Currency = updatedBudget.Currency; + item.StartDate = updatedBudget.StartDate; + item.EndDate = updatedBudget.EndDate; + } + } + } + } + + private void OnInternalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + BudgetsChanged?.Invoke(this, e); + } + } +} diff --git a/FinTrack/Services/Budgets/IBudgetService.cs b/FinTrack/Services/Budgets/IBudgetService.cs deleted file mode 100644 index 23177a2..0000000 --- a/FinTrack/Services/Budgets/IBudgetService.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FinTrack.Services.Budgets -{ - public interface IBudgetService - { - /* - List GetBudgets(); - void AddBudget(string name, decimal amount, DateTime dueDate); - void UpdateBudget(int budgetId, string name, decimal amount, DateTime dueDate); - void DeleteBudget(int budgetId); - */ - } -} diff --git a/FinTrack/Services/Budgets/IBudgetStore.cs b/FinTrack/Services/Budgets/IBudgetStore.cs new file mode 100644 index 0000000..2f32696 --- /dev/null +++ b/FinTrack/Services/Budgets/IBudgetStore.cs @@ -0,0 +1,20 @@ +using FinTrackForWindows.Dtos.BudgetDtos; +using FinTrackForWindows.Models.Budget; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace FinTrackForWindows.Services.Budgets +{ + public interface IBudgetStore + { + ReadOnlyObservableCollection Budgets { get; } + + event NotifyCollectionChangedEventHandler? BudgetsChanged; + + Task LoadBudgetsAsync(); + + Task AddBudgetAsync(BudgetCreateDto newBudget); + Task UpdateBudgetAsync(int budgetId, BudgetCreateDto updatedBudget); + Task DeleteBudgetAsync(int budgetId); + } +} diff --git a/FinTrack/Services/Currencies/CurrenciesStore.cs b/FinTrack/Services/Currencies/CurrenciesStore.cs new file mode 100644 index 0000000..bcdad83 --- /dev/null +++ b/FinTrack/Services/Currencies/CurrenciesStore.cs @@ -0,0 +1,104 @@ +using FinTrackForWindows.Dtos.CurrencyDtos; +using FinTrackForWindows.Models.Currency; +using FinTrackForWindows.Services.Api; +using FinTrackWebApi.Dtos.CurrencyDtos; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Currencies +{ + public class CurrenciesStore : ICurrenciesStore + { + private readonly IApiService _apiService; + private readonly ILogger _logger; + private readonly ObservableCollection _currencies; + + public ReadOnlyObservableCollection Currencies { get; } + + public CurrenciesStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + + _currencies = new ObservableCollection(); + Currencies = new ReadOnlyObservableCollection(_currencies); + } + + public async Task LoadCurrenciesAsync() + { + if (_currencies.Any()) + { + _logger.LogInformation("Para birimleri zaten yüklü. API çağrısı atlanıyor."); + return; + } + + try + { + var response = await _apiService.GetAsync("Currency/latest/USD"); + if (response?.Rates != null) + { + _currencies.Clear(); + foreach (var item in response.Rates) + { + _currencies.Add(new CurrencyModel + { + Id = item.Id, + ToCurrencyCode = item.Code, + ToCurrencyName = item.CountryCode ?? "N/A", + ToCurrencyFlag = item.IconUrl ?? string.Empty, + ToCurrencyPrice = item.Rate + }); + } + _logger.LogInformation("{Count} adet para birimi CurrenciesStore'a yüklendi.", _currencies.Count); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "CurrenciesStore'da para birimi listesi yüklenirken hata oluştu."); + } + } + + public async Task GetHistoricalDataAsync(string targetCurrencyCode, string period) + { + try + { + _logger.LogInformation("{Target} için geçmiş veriler {Period} periyoduyla isteniyor.", targetCurrencyCode, period); + + string baseCurrency = "USD"; + string endpoint = $"Currency/{baseCurrency}/history/{targetCurrencyCode}?period={period}"; + var historyData = await _apiService.GetAsync(endpoint); + + return historyData; + } + catch (Exception ex) + { + _logger.LogError(ex, "{Target} için geçmiş veriler yüklenirken CurrenciesStore'da hata oluştu.", targetCurrencyCode); + return null; + } + } + + public async Task GetConvertCurrencies(string fromCurrencyCode, string toCurrencyCode, decimal amount) + { + try + { + _logger.LogInformation("{From} para biriminden {To} para birimine {Amount} miktarı dönüştürülüyor.", fromCurrencyCode, toCurrencyCode, amount); + string endpoint = $"Currency/convert?from={fromCurrencyCode}&to={toCurrencyCode}&amount={amount}"; + var response = await _apiService.GetAsync(endpoint); + if (response != null) + { + return response.Result; + } + else + { + _logger.LogWarning("Dönüştürme işlemi için geçerli bir yanıt alınamadı."); + return 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "{From} para biriminden {To} para birimine dönüştürme işlemi sırasında hata oluştu.", fromCurrencyCode, toCurrencyCode); + return 0; + } + } + } +} \ No newline at end of file diff --git a/FinTrack/Services/Currencies/ICurrenciesStore.cs b/FinTrack/Services/Currencies/ICurrenciesStore.cs new file mode 100644 index 0000000..0e25ced --- /dev/null +++ b/FinTrack/Services/Currencies/ICurrenciesStore.cs @@ -0,0 +1,14 @@ +using FinTrackForWindows.Models.Currency; +using FinTrackWebApi.Dtos.CurrencyDtos; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Currencies +{ + public interface ICurrenciesStore + { + ReadOnlyObservableCollection Currencies { get; } + Task LoadCurrenciesAsync(); + Task GetHistoricalDataAsync(string targetCurrencyCode, string period); + Task GetConvertCurrencies(string fromCurrencyCode, string toCurrencyCode, decimal amount); + } +} diff --git a/FinTrack/Services/Debts/DebtStore.cs b/FinTrack/Services/Debts/DebtStore.cs new file mode 100644 index 0000000..5694041 --- /dev/null +++ b/FinTrack/Services/Debts/DebtStore.cs @@ -0,0 +1,183 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using FinTrackForWindows.Core; +using FinTrackForWindows.Dtos.DebtDtos; +using FinTrackForWindows.Enums; +using FinTrackForWindows.Models.Debt; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace FinTrackForWindows.Services.Debts +{ + public partial class DebtStore : ObservableObject, IDebtStore + { + private readonly IApiService _apiService; + private readonly ILogger _logger; + private readonly int _currentUserId; + + [ObservableProperty] + private bool _isLoading; + + private readonly ObservableCollection _pendingOffers; + public ReadOnlyObservableCollection PendingOffers { get; } + + private readonly ObservableCollection _myDebtsList; + public ReadOnlyObservableCollection MyDebtsList { get; } + + private readonly ObservableCollection _activeDebts; + public ReadOnlyObservableCollection ActiveDebts { get; } + + public event Action? DebtsChanged; + + public DebtStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + + _activeDebts = new ObservableCollection(); + ActiveDebts = new ReadOnlyObservableCollection(_activeDebts); + + var handler = new JwtSecurityTokenHandler(); + var jsonToken = handler.ReadJwtToken(SessionManager.CurrentToken); + _currentUserId = Convert.ToInt32(jsonToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value); + + _pendingOffers = new ObservableCollection(); + PendingOffers = new ReadOnlyObservableCollection(_pendingOffers); + + _myDebtsList = new ObservableCollection(); + MyDebtsList = new ReadOnlyObservableCollection(_myDebtsList); + } + + public async Task LoadDebtsAsync() + { + IsLoading = true; + _logger.LogInformation("Loading debt data from API..."); + try + { + var debtDtos = await _apiService.GetAsync>("Debt"); + if (debtDtos == null) return; + + _pendingOffers.Clear(); + _myDebtsList.Clear(); + _activeDebts.Clear(); + + foreach (var dto in debtDtos) + { + var debtModel = new DebtModel + { + Id = dto.Id, + LenderId = dto.LenderId, + BorrowerId = dto.BorrowerId, + CurrentUserId = _currentUserId, + LenderName = dto.LenderName, + BorrowerName = dto.BorrowerName, + borrowerImageUrl = dto.BorrowerProfilePicture ?? "/Assets/Images/Icons/user-red.png", + lenderImageUrl = dto.LenderProfilePicture ?? "/Assets/Images/Icons/user-green.png", + Amount = dto.Amount, + DueDate = dto.DueDateUtc.ToLocalTime(), + Status = dto.Status + }; + + if (dto.Status == DebtStatusType.PendingBorrowerAcceptance && dto.BorrowerId == _currentUserId) + { + _pendingOffers.Add(debtModel); + } + else + { + _myDebtsList.Add(debtModel); + } + + if (dto.Status == DebtStatusType.Active) + { + _activeDebts.Add(debtModel); + } + } + DebtsChanged?.Invoke(); + _logger.LogInformation("Successfully loaded and processed {Count} debts.", debtDtos.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load debt data."); + } + finally + { + IsLoading = false; + } + } + + public async Task SendOfferAsync(string borrowerEmail, decimal amount, string currency, DateTime dueDate, string description) + { + IsLoading = true; + try + { + var createDto = new CreateDebtOfferRequestDto + { + BorrowerEmail = borrowerEmail, + Amount = amount, + CurrencyCode = BaseCurrencyType.TRY, + DueDateUtc = dueDate.ToUniversalTime(), + Description = description + }; + + _logger.LogInformation("Sending new debt offer to API..."); + await _apiService.PostAsync("Debt/create-debt-offer", createDto); + await LoadDebtsAsync(); + _logger.LogInformation("Debt offer sent successfully."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send debt offer."); + throw; + } + finally + { + IsLoading = false; + } + } + + public async Task RespondToOfferAsync(DebtModel debt, bool accepted) + { + IsLoading = true; + try + { + _logger.LogInformation("Responding to offer for DebtId: {DebtId} with decision: {Decision}", debt.Id, accepted); + var requestBody = new { Accepted = accepted }; + await _apiService.PostAsync($"Debt/respond-to-offer/{debt.Id}", requestBody); + await LoadDebtsAsync(); + _logger.LogInformation("Successfully responded to offer for DebtId: {DebtId}", debt.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to respond to debt offer for DebtId: {DebtId}", debt.Id); + throw; + } + finally + { + IsLoading = false; + } + } + + public async Task UploadVideoAsync(DebtModel debt, string filePath) + { + IsLoading = true; + try + { + _logger.LogInformation("Uploading video for DebtId: {DebtId}", debt.Id); + await _apiService.UploadFileAsync($"Videos/user-upload-video?debtId={debt.Id}", filePath); + await LoadDebtsAsync(); + _logger.LogInformation("Video uploaded successfully for DebtId: {DebtId}", debt.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to upload video for DebtId: {DebtId}", debt.Id); + throw; + } + finally + { + IsLoading = false; + } + } + } +} \ No newline at end of file diff --git a/FinTrack/Services/Debts/IDebtStore.cs b/FinTrack/Services/Debts/IDebtStore.cs new file mode 100644 index 0000000..c60cdf2 --- /dev/null +++ b/FinTrack/Services/Debts/IDebtStore.cs @@ -0,0 +1,21 @@ +using FinTrackForWindows.Models.Debt; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Debts +{ + public interface IDebtStore + { + ReadOnlyObservableCollection PendingOffers { get; } + ReadOnlyObservableCollection MyDebtsList { get; } + ReadOnlyObservableCollection ActiveDebts { get; } + + bool IsLoading { get; } + + Task LoadDebtsAsync(); + Task SendOfferAsync(string borrowerEmail, decimal amount, string currency, DateTime dueDate, string description); + Task RespondToOfferAsync(DebtModel debt, bool accepted); + Task UploadVideoAsync(DebtModel debt, string filePath); + + event Action DebtsChanged; + } +} \ No newline at end of file diff --git a/FinTrack/Services/Memberships/IMembershipStore.cs b/FinTrack/Services/Memberships/IMembershipStore.cs new file mode 100644 index 0000000..a15fc8f --- /dev/null +++ b/FinTrack/Services/Memberships/IMembershipStore.cs @@ -0,0 +1,16 @@ +using FinTrackForWindows.Dtos.MembershipDtos; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Memberships +{ + public interface IMembershipStore + { + UserMembershipDto CurrentUserMembership { get; } + ReadOnlyObservableCollection AvailablePlans { get; } + ReadOnlyObservableCollection MembershipHistory { get; } + Task LoadAllMembershipDataAsync(); + Task SelectPlanAsync(int planId, bool autoRenew); + Task CancelCurrentSubscriptionAsync(); + event Action CurrentUserMembershipChanged; + } +} diff --git a/FinTrack/Services/Memberships/MembershipStore.cs b/FinTrack/Services/Memberships/MembershipStore.cs new file mode 100644 index 0000000..0c495b0 --- /dev/null +++ b/FinTrack/Services/Memberships/MembershipStore.cs @@ -0,0 +1,123 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using FinTrackForWindows.Dtos.MembershipDtos; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Memberships +{ + public partial class MembershipStore : ObservableObject, IMembershipStore + { + private readonly IApiService _apiService; + private readonly ILogger _logger; + + [ObservableProperty] + private UserMembershipDto _currentUserMembership; + + private readonly ObservableCollection _availablePlans; + public ReadOnlyObservableCollection AvailablePlans { get; } + + private readonly ObservableCollection _membershipHistory; + public ReadOnlyObservableCollection MembershipHistory { get; } + + public event Action CurrentUserMembershipChanged; + + public MembershipStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + + _availablePlans = new ObservableCollection(); + AvailablePlans = new ReadOnlyObservableCollection(_availablePlans); + + _membershipHistory = new ObservableCollection(); + MembershipHistory = new ReadOnlyObservableCollection(_membershipHistory); + } + + public async Task LoadAllMembershipDataAsync() + { + _logger.LogInformation("Loading all membership data from API..."); + try + { + Task> plansTask = _apiService.GetAsync>("Membership/plans"); + Task currentMembershipTask = _apiService.GetAsync("Membership/current"); + Task> historyTask = _apiService.GetAsync>("Membership/history"); + + await Task.WhenAll(plansTask, currentMembershipTask, historyTask); + + _availablePlans.Clear(); + var plans = await plansTask; + if (plans != null) + { + foreach (var plan in plans.OrderBy(p => p.Price)) + { + _availablePlans.Add(plan); + } + } + + CurrentUserMembership = await currentMembershipTask; + + _membershipHistory.Clear(); + var history = await historyTask; + if (history != null) + { + foreach (var item in history.OrderByDescending(h => h.StartDate)) + { + _membershipHistory.Add(item); + } + } + + CurrentUserMembershipChanged?.Invoke(); + _logger.LogInformation("All membership data loaded successfully."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load all membership data."); + CurrentUserMembership = null; + _availablePlans.Clear(); + _membershipHistory.Clear(); + CurrentUserMembershipChanged?.Invoke(); + } + } + + public async Task SelectPlanAsync(int planId, bool autoRenew) + { + _logger.LogInformation($"Initiating plan selection for Plan ID: {planId}."); + try + { + var request = new SubscriptionRequestDto { PlanId = planId, AutoRenew = autoRenew }; + + var responseDto = await _apiService.PostAsync("Membership/create-checkout-session", request); + + await LoadAllMembershipDataAsync(); + + return responseDto?.CheckoutUrl ?? "N/A"; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while selecting plan ID: {PlanId}.", planId); + return string.Empty; + } + } + + public async Task CancelCurrentSubscriptionAsync() + { + if (CurrentUserMembership == null) + { + _logger.LogWarning("Attempted to cancel a subscription, but no active subscription found."); + return; + } + + _logger.LogInformation($"Initiating cancellation for subscription ID: {CurrentUserMembership.Id}."); + try + { + await _apiService.PostAsync($"Membership/{CurrentUserMembership.Id}/cancel", null); + await LoadAllMembershipDataAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while cancelling subscription ID: {SubscriptionId}.", CurrentUserMembership.Id); + } + } + } +} \ No newline at end of file diff --git a/FinTrack/Services/Transactions/ITransactionStore.cs b/FinTrack/Services/Transactions/ITransactionStore.cs new file mode 100644 index 0000000..f96f7ad --- /dev/null +++ b/FinTrack/Services/Transactions/ITransactionStore.cs @@ -0,0 +1,20 @@ +using FinTrackForWindows.Dtos.TransactionDtos; +using FinTrackForWindows.Models.Transaction; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace FinTrackForWindows.Services.Transactions +{ + public interface ITransactionStore + { + ReadOnlyObservableCollection Transactions { get; } + + event NotifyCollectionChangedEventHandler? TransactionsChanged; + + Task LoadTransactionsAsync(); + + Task AddTransactionAsync(TransactionCreateDto newTransaction); + Task UpdateTransactionAsync(int transactionId, TransactionCreateDto updatedTransaction); + Task DeleteTransactionAsync(int transactionId); + } +} diff --git a/FinTrack/Services/Transactions/TransactionStore.cs b/FinTrack/Services/Transactions/TransactionStore.cs new file mode 100644 index 0000000..89d6245 --- /dev/null +++ b/FinTrack/Services/Transactions/TransactionStore.cs @@ -0,0 +1,126 @@ +using FinTrackForWindows.Dtos.TransactionDtos; +using FinTrackForWindows.Models.Transaction; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace FinTrackForWindows.Services.Transactions +{ + public class TransactionStore : ITransactionStore + { + private readonly IApiService _apiService; + + private readonly ILogger _logger; + + private readonly ObservableCollection _transactions; + + public ReadOnlyObservableCollection Transactions { get; } + + public TransactionStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + _transactions = new ObservableCollection(); + Transactions = new ReadOnlyObservableCollection(_transactions); + + _transactions.CollectionChanged += OnInternalCollectionChanged; + } + + public event NotifyCollectionChangedEventHandler? TransactionsChanged; + + public async Task AddTransactionAsync(TransactionCreateDto newTransaction) + { + var createdTransactionDto = await _apiService.PostAsync("Transactions", newTransaction); + if (createdTransactionDto != null) + { + _transactions.Add(new TransactionModel + { + Id = createdTransactionDto.Id, + AccountId = createdTransactionDto.Account.Id, + CategoryId = createdTransactionDto.Category.Id, + NameOrDescription = createdTransactionDto.Description ?? "N/A", + Amount = createdTransactionDto.Amount, + Type = createdTransactionDto.Category.Type, + Date = createdTransactionDto.TransactionDateUtc.ToLocalTime(), + AccountName = createdTransactionDto.Account.Name, + CategoryName = createdTransactionDto.Category.Name, + Currency = createdTransactionDto.Currency, + }); + } + } + + public async Task DeleteTransactionAsync(int transactionId) + { + await _apiService.DeleteAsync($"Transactions/{transactionId}"); + var transactionToRemove = _transactions.FirstOrDefault(b => b.Id == transactionId); + if (transactionToRemove != null) + { + _transactions.Remove(transactionToRemove); + } + } + + public async Task LoadTransactionsAsync() + { + if (_transactions.Any()) + { + _logger.LogInformation("İşlemler zaten yüklü. API çağrısı atlanıyor."); + return; + } + + try + { + var TransactionFromApi = await _apiService.GetAsync>("Transactions"); + if (TransactionFromApi == null) return; + + _transactions.Clear(); + foreach (var dto in TransactionFromApi) + { + _transactions.Add(new TransactionModel + { + Id = dto.Id, + NameOrDescription = dto.Description ?? "N/A", + Amount = dto.Amount, + Date = dto.TransactionDateUtc, + AccountName = dto.Account.Name, + CategoryId = dto.Category.Id, + CategoryName = dto.Category.Name, + Type = dto.Category.Type, + Currency = dto.Currency + }); + } + _logger.LogInformation("{Count} adet işlem Transactionstore'a yüklendi.", _transactions.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Transactionstore'da işlemler yüklenirken hata oluştu."); + } + } + + public async Task UpdateTransactionAsync(int transactionId, TransactionCreateDto updatedTransaction) + { + var updateTransactionDto = await _apiService.PutAsync($"Transactions/{transactionId}", updatedTransaction); + if (updateTransactionDto != null) + { + var itemToUpdate = _transactions.FirstOrDefault(t => t.Id == transactionId); + if (itemToUpdate != null) + { + itemToUpdate.NameOrDescription = updateTransactionDto.Description ?? "N/A"; + itemToUpdate.Amount = updateTransactionDto.Amount; + itemToUpdate.Type = updateTransactionDto.Category.Type; + itemToUpdate.Date = updateTransactionDto.TransactionDateUtc.ToLocalTime(); + itemToUpdate.AccountName = updateTransactionDto.Account.Name; + itemToUpdate.CategoryName = updateTransactionDto.Category.Name; + itemToUpdate.Currency = updateTransactionDto.Currency; + itemToUpdate.AccountId = updateTransactionDto.Account.Id; + itemToUpdate.CategoryId = updateTransactionDto.Category.Id; + } + } + } + + private void OnInternalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + TransactionsChanged?.Invoke(this, e); + } + } +} diff --git a/FinTrack/Styles/ModernStyles.xaml b/FinTrack/Styles/ModernStyles.xaml index 672002c..f8d5453 100644 --- a/FinTrack/Styles/ModernStyles.xaml +++ b/FinTrack/Styles/ModernStyles.xaml @@ -699,7 +699,6 @@ - - + + + + + + + + + - + \ No newline at end of file diff --git a/FinTrack/ViewModels/AccountViewModel.cs b/FinTrack/ViewModels/AccountViewModel.cs index 1393198..ee67213 100644 --- a/FinTrack/ViewModels/AccountViewModel.cs +++ b/FinTrack/ViewModels/AccountViewModel.cs @@ -4,6 +4,7 @@ using FinTrackForWindows.Dtos.TransactionDtos; using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Account; +using FinTrackForWindows.Services.Accounts; using FinTrackForWindows.Services.Api; using LiveChartsCore; using LiveChartsCore.Defaults; @@ -13,13 +14,13 @@ using Microsoft.Extensions.Logging; using SkiaSharp; using System.Collections.ObjectModel; +using System.Windows; namespace FinTrackForWindows.ViewModels { public partial class AccountViewModel : ObservableObject { - [ObservableProperty] - private ObservableCollection accounts = new(); + public ReadOnlyObservableCollection Accounts => _accountStore.Accounts; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle))] @@ -32,6 +33,8 @@ public partial class AccountViewModel : ObservableObject private readonly ILogger _logger; + private readonly IAccountStore _accountStore; + public string FormTitle => IsEditing ? "Hesabı Düzenle" : "Yeni Hesap Ekle"; public string SaveButtonText => IsEditing ? "GÜNCELLE" : "HESAP OLUŞTUR"; @@ -51,10 +54,11 @@ public partial class AccountViewModel : ObservableObject [ObservableProperty] private LabelVisual title = new LabelVisual { /* Başlık için yer tutucu */ }; - public AccountViewModel(ILogger logger, IApiService apiService) + public AccountViewModel(ILogger logger, IApiService apiService, IAccountStore accountStore) { _logger = logger; _apiService = apiService; + _accountStore = accountStore; InitializeEmptyChart(); _ = LoadData(); @@ -121,22 +125,7 @@ partial void OnSelectedAccountChanged(AccountModel? value) private async Task LoadData() { - var accounts = await _apiService.GetAsync>("Account"); - Accounts = new ObservableCollection(); - if (accounts != null) - { - foreach (var item in accounts) - { - Accounts.Add(new AccountModel - { - Id = item.Id, - Name = item.Name, - Type = item.Type, - balance = item.Balance, - Currency = item.Currency, - }); - } - } + await _accountStore.LoadAccountsAsync(); } private async Task LoadTransactionHistory(int accountId, string accountName) @@ -238,48 +227,41 @@ private async Task SaveAccount() { if (SelectedAccount == null || string.IsNullOrWhiteSpace(SelectedAccount.Name)) return; - if (IsEditing) + var accountDto = new AccountCreateDto { - var existingAccount = Accounts.FirstOrDefault(a => a.Id == SelectedAccount.Id); - if (existingAccount != null) - { - existingAccount.Name = SelectedAccount.Name; - existingAccount.Type = SelectedAccount.Type; - existingAccount.Currency = SelectedAccount.Currency; + Name = SelectedAccount.Name, + Type = SelectedAccount.Type, + Currency = SelectedAccount.Currency, + }; - await _apiService.PutAsync($"Account/{SelectedAccount.Id}", new AccountUpdateDto - { - Name = SelectedAccount.Name, - Type = SelectedAccount.Type, - Currency = SelectedAccount.Currency - }); - } - } - else + if (IsEditing && SelectedAccount.Id.HasValue) { - var newAccount = await _apiService.PostAsync("Account", new AccountCreateDto - { - Name = SelectedAccount.Name, - Type = SelectedAccount.Type, - IsActive = true, - Currency = SelectedAccount.Currency, - }); + int accountId = SelectedAccount.Id.Value; + string accountName = SelectedAccount.Name; - SelectedAccount.Id = newAccount.Id; + await _accountStore.UpdateAccountAsync(SelectedAccount.Id ?? -1, accountDto); - Accounts.Add(SelectedAccount); + await LoadTransactionHistory(accountId, accountName); + } + else + { + await _accountStore.AddAccountAsync(accountDto); } + PrepareForNewAccount(); } [RelayCommand] - private async Task DeleteAccount(AccountModel accountToDelete) + private async Task DeleteAccount(AccountModel? accountToDelete) { + if (accountToDelete == null) return; + + var result = MessageBox.Show($"'{accountToDelete.Name}' adlı hesabı silmek istediğinizden emin misiniz?", "Silme Onayı", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.No) return; + if (accountToDelete != null) { - Accounts.Remove(accountToDelete); - - await _apiService.DeleteAsync($"Account/{accountToDelete.Id}"); + await _accountStore.DeleteAccountAsync(SelectedAccount?.Id ?? -1); if (SelectedAccount?.Id == accountToDelete.Id) { diff --git a/FinTrack/ViewModels/BudgetViewModel.cs b/FinTrack/ViewModels/BudgetViewModel.cs index bdda353..cc7d4ab 100644 --- a/FinTrack/ViewModels/BudgetViewModel.cs +++ b/FinTrack/ViewModels/BudgetViewModel.cs @@ -1,9 +1,11 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Dtos.BudgetDtos; +using FinTrackForWindows.Dtos.CategoryDtos; using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Budget; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.Budgets; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Windows; @@ -12,8 +14,7 @@ namespace FinTrackForWindows.ViewModels { public partial class BudgetViewModel : ObservableObject { - [ObservableProperty] - private ObservableCollection budgets; + public ReadOnlyObservableCollection Budgets => _budgetStore.Budgets; [ObservableProperty] private BudgetModel? selectedBudget; @@ -25,53 +26,51 @@ public partial class BudgetViewModel : ObservableObject public ObservableCollection Categories { get; } public IEnumerable CurrencyTypes => Enum.GetValues(typeof(BaseCurrencyType)).Cast(); + public string FormTitle => IsEditing ? "Bütçeyi Düzenle" : "Yeni Bütçe Ekle"; + public string SaveButtonText => IsEditing ? "BÜTÇEYİ GÜNCELLE" : "BÜTÇE OLUŞTUR"; - public string FormTitle => IsEditing ? "Edit Budget" : "Add New Budget"; - public string SaveButtonText => IsEditing ? "UPDATE THE BUDGET" : "CREATE A BUDGET"; - + private readonly IBudgetStore _budgetStore; private readonly ILogger _logger; private readonly IApiService _apiService; - public BudgetViewModel(ILogger logger, IApiService apiService) + public BudgetViewModel(IBudgetStore budgetStore, ILogger logger, IApiService apiService) { + _budgetStore = budgetStore; _logger = logger; _apiService = apiService; - Budgets = new ObservableCollection(); - Categories = new ObservableCollection { "Gıda", "Fatura", "Ulaşım", "Eğlence", "Sağlık", "Diğer" }; + Categories = new ObservableCollection(); - _ = LoadBudgetsAsync(); + _ = InitializeViewModelAsync(); PrepareForNewBudget(); } - private async Task LoadBudgetsAsync() + private async Task InitializeViewModelAsync() + { + await _budgetStore.LoadBudgetsAsync(); + await LoadCategoriesAsync(); + } + + private async Task LoadCategoriesAsync() { try { - var budgetsFromApi = await _apiService.GetAsync>("Budgets"); - if (budgetsFromApi == null) return; + var categoriesFromApi = await _apiService.GetAsync>("Categories"); + if (categoriesFromApi == null) return; - Budgets.Clear(); - foreach (var dto in budgetsFromApi) + App.Current.Dispatcher.Invoke(() => { - Budgets.Add(new BudgetModel + Categories.Clear(); + foreach (var category in categoriesFromApi.OrderBy(c => c.Name)) { - Id = dto.Id, - Name = dto.Name, - Description = dto.Description, - Category = dto.Category, - AllocatedAmount = dto.AllocatedAmount, - CurrentAmount = 0, // TODO: API'den gelen veride mevcut tutar yoksa 0 olarak ayarla - Currency = dto.Currency, - StartDate = dto.StartDate, - EndDate = dto.EndDate - }); - } + Categories.Add(category.Name); + } + }); } catch (Exception ex) { - _logger.LogError(ex, "Bütçeler yüklenirken bir hata oluştu."); - MessageBox.Show("Bütçeler yüklenemedi. Lütfen internet bağlantınızı kontrol edin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.LogError(ex, "Kategoriler yüklenirken hata oluştu."); + MessageBox.Show("Kategoriler yüklenemedi. Lütfen internet bağlantınızı kontrol edin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -80,7 +79,14 @@ private async Task SaveBudgetAsync() { if (SelectedBudget == null || string.IsNullOrWhiteSpace(SelectedBudget.Name) || SelectedBudget.AllocatedAmount <= 0) { - MessageBox.Show("Lütfen bütçe adı ve 0'dan büyük bir hedef tutar girin.", "Eksik Bilgi", MessageBoxButton.OK, MessageBoxImage.Warning); + MessageBox.Show("Lütfen bütçe adı ve sıfırdan büyük bir miktar girin.", "Eksik Bilgi", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + string? categoryToSave = SelectedBudget.Category?.Trim(); + if (string.IsNullOrWhiteSpace(categoryToSave)) + { + MessageBox.Show("Lütfen bir kategori seçin veya yazın.", "Eksik Bilgi", MessageBoxButton.OK, MessageBoxImage.Warning); return; } @@ -88,7 +94,7 @@ private async Task SaveBudgetAsync() { Name = SelectedBudget.Name, Description = SelectedBudget.Description, - Category = SelectedBudget.Category, + Category = categoryToSave, AllocatedAmount = SelectedBudget.AllocatedAmount, Currency = SelectedBudget.Currency, StartDate = SelectedBudget.StartDate, @@ -100,29 +106,20 @@ private async Task SaveBudgetAsync() { if (IsEditing) { - // TEST - await _apiService.PutAsync($"Budgets/{SelectedBudget.Id}", budgetDto); - _logger.LogInformation("Bütçe güncellendi: {BudgetId}", SelectedBudget.Id); - - var existingBudget = Budgets.FirstOrDefault(b => b.Id == SelectedBudget.Id); - if (existingBudget != null) - { - var index = Budgets.IndexOf(existingBudget); - Budgets[index] = SelectedBudget; - } + await _budgetStore.UpdateBudgetAsync(SelectedBudget.Id, budgetDto); } else { - var createdBudgetDto = await _apiService.PostAsync("Budgets", budgetDto); - _logger.LogInformation("Yeni bütçe oluşturuldu."); - Budgets.Add(SelectedBudget); + await _budgetStore.AddBudgetAsync(budgetDto); } + + await LoadCategoriesAsync(); PrepareForNewBudget(); } catch (Exception ex) { - _logger.LogError(ex, "Bütçe kaydedilirken bir hata oluştu."); - MessageBox.Show("Bütçe kaydedilemedi.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.LogError(ex, "Bütçe kaydedilirken hata oluştu."); + MessageBox.Show("Bütçe kaydedilemedi. Lütfen internet bağlantınızı kontrol edin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -136,9 +133,7 @@ private async Task DeleteBudgetAsync(BudgetModel? budgetToDelete) try { - await _apiService.DeleteAsync($"Budgets/{budgetToDelete.Id}"); - Budgets.Remove(budgetToDelete); - _logger.LogInformation("Bütçe silindi: {BudgetId}", budgetToDelete.Id); + await _budgetStore.DeleteBudgetAsync(budgetToDelete.Id); if (SelectedBudget?.Id == budgetToDelete.Id) { diff --git a/FinTrack/ViewModels/CurrenciesViewModel.cs b/FinTrack/ViewModels/CurrenciesViewModel.cs index fd66a05..9057761 100644 --- a/FinTrack/ViewModels/CurrenciesViewModel.cs +++ b/FinTrack/ViewModels/CurrenciesViewModel.cs @@ -1,7 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Currency; -using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.Currencies; using FinTrackWebApi.Dtos.CurrencyDtos; using LiveChartsCore; using LiveChartsCore.Defaults; @@ -21,8 +22,6 @@ namespace FinTrackForWindows.ViewModels { public partial class CurrenciesViewModel : ObservableObject { - private ObservableCollection allCurrencies = new(); - [ObservableProperty] private ObservableCollection filteredCurrencies = new(); @@ -38,6 +37,9 @@ public partial class CurrenciesViewModel : ObservableObject public bool IsCurrencySelected => SelectedCurrency != null; + [ObservableProperty] + private string selectedPeriod = "1M"; + [ObservableProperty] private ISeries[] series = new ISeries[0]; @@ -75,15 +77,16 @@ public partial class CurrenciesViewModel : ObservableObject private double periodLowValue = 0; private readonly ILogger _logger; - private readonly IApiService _apiService; + private readonly ICurrenciesStore _currenciesStore; private static readonly SKColor _chartColor = SKColor.Parse("#3498DB"); - public CurrenciesViewModel(ILogger logger, IApiService apiService) + public CurrenciesViewModel(ILogger logger, ICurrenciesStore currenciesStore) { _logger = logger; - _apiService = apiService; + _currenciesStore = currenciesStore; + InitializeEmptyChart(); - _ = LoadCurrenciesData(); + _ = InitializeDataAsync(); } partial void OnCurrencySearchChanged(string value) @@ -101,47 +104,31 @@ async partial void OnSelectedCurrencyChanged(CurrencyModel? value) await LoadHistoricalDataAsync(value.ToCurrencyCode); } - private async Task LoadCurrenciesData() + private async Task InitializeDataAsync() { try { - var response = await _apiService.GetAsync("Currency/latest/USD"); - if (response?.Rates != null) + await _currenciesStore.LoadCurrenciesAsync(); + FilterCurrencies(); + + if (FilteredCurrencies.Any()) { - allCurrencies.Clear(); - foreach (var item in response.Rates) - { - allCurrencies.Add(new CurrencyModel - { - Id = item.Id, - ToCurrencyCode = item.Code, - ToCurrencyName = item.CountryCode ?? "N/A", - ToCurrencyFlag = item.IconUrl ?? string.Empty, - ToCurrencyPrice = item.Rate - }); - } - FilterCurrencies(); - if (FilteredCurrencies.Any()) - { - SelectedCurrency = FilteredCurrencies.First(); - } + SelectedCurrency = FilteredCurrencies.First(); } } catch (Exception ex) { - _logger.LogError(ex, "Para birimi listesi yüklenirken hata oluştu."); + _logger.LogError(ex, "Para birimi verileri başlatılırken hata oluştu."); InitializeEmptyChart("Failed to load currency list."); } } - private async Task LoadHistoricalDataAsync(string targetCurrencyCode, string period = "1M") + private async Task LoadHistoricalDataAsync(string targetCurrencyCode) { IsLoadingDetails = true; try { - string baseCurrency = "USD"; - string endpoint = $"Currency/{baseCurrency}/history/{targetCurrencyCode}?period={period}"; - var historyData = await _apiService.GetAsync(endpoint); + var historyData = await _currenciesStore.GetHistoricalDataAsync(targetCurrencyCode, SelectedPeriod); if (historyData == null || SelectedCurrency == null || !historyData.HistoricalRates.Any()) { @@ -150,15 +137,15 @@ private async Task LoadHistoricalDataAsync(string targetCurrencyCode, string per return; } - _logger.LogInformation("{Target} için geçmiş veriler yüklendi.", targetCurrencyCode); + _logger.LogInformation("{Target} için geçmiş veriler ViewModel'de işleniyor.", targetCurrencyCode); var dailyHistoricalRates = UpdateChart(historyData); UpdateAdditionalVisuals(dailyHistoricalRates); UpdateDetails(historyData.ChangeSummary); } catch (Exception ex) { - _logger.LogError(ex, "{Target} için geçmiş veriler yüklenirken hata oluştu.", targetCurrencyCode); - InitializeEmptyChart($"Error loading data for {targetCurrencyCode}."); + _logger.LogError(ex, "ViewModel'de geçmiş veriler işlenirken hata oluştu."); + InitializeEmptyChart($"Error processing data for {targetCurrencyCode}."); UpdateDetails(null); } finally @@ -167,6 +154,20 @@ private async Task LoadHistoricalDataAsync(string targetCurrencyCode, string per } } + [RelayCommand] + private async Task ChangePeriod(string? newPeriod) + { + if (string.IsNullOrWhiteSpace(newPeriod) || SelectedPeriod == newPeriod || SelectedCurrency == null) + { + return; + } + + _logger.LogInformation("Grafik periyodu {NewPeriod} olarak değiştiriliyor.", newPeriod); + SelectedPeriod = newPeriod; + + await LoadHistoricalDataAsync(SelectedCurrency.ToCurrencyCode); + } + private List UpdateChart(CurrencyHistoryDto historyData) { var dailyHistoricalRates = historyData.HistoricalRates @@ -261,28 +262,13 @@ private void UpdateAdditionalVisuals(List dailyRates) GaugeSeries = new ISeries[] { - new PieSeries - { - Values = new double[] { currentRate }, - InnerRadius = 50, - Fill = new SolidColorPaint(_chartColor), - IsHoverable = false, - }, - new PieSeries - { - Values = new double[] { periodHigh - currentRate }, - InnerRadius = 50, - Fill = new SolidColorPaint(SKColors.DarkGray), - IsHoverable = false, - } + new PieSeries { Values = new double[] { currentRate }, InnerRadius = 50, Fill = new SolidColorPaint(_chartColor), IsHoverable = false, }, + new PieSeries { Values = new double[] { periodHigh - currentRate }, InnerRadius = 50, Fill = new SolidColorPaint(SKColors.DarkGray), IsHoverable = false, } }; GaugeVisuals = Enumerable.Empty>(); - int upDays = 0; - int downDays = 0; - int noChangeDays = 0; - + int upDays = 0, downDays = 0, noChangeDays = 0; for (int i = 1; i < dailyRates.Count; i++) { if (dailyRates[i].Rate > dailyRates[i - 1].Rate) upDays++; @@ -306,7 +292,6 @@ private void ClearAdditionalVisuals() PeriodHighText = "N/A"; PeriodLowText = "N/A"; AverageRateText = "N/A"; - PeriodHighValue = 1; PeriodLowValue = 0; } @@ -376,14 +361,15 @@ private void InitializeEmptyChart(string message = "Select a currency to view it private void FilterCurrencies() { + var sourceList = _currenciesStore.Currencies; if (string.IsNullOrWhiteSpace(CurrencySearch)) { - FilteredCurrencies = new ObservableCollection(allCurrencies); + FilteredCurrencies = new ObservableCollection(sourceList); } else { var searchText = CurrencySearch.ToLowerInvariant(); - var filtered = allCurrencies + var filtered = sourceList .Where(c => c.ToCurrencyCode.ToLowerInvariant().Contains(searchText) || c.ToCurrencyName.ToLowerInvariant().Contains(searchText)); FilteredCurrencies = new ObservableCollection(filtered); diff --git a/FinTrack/ViewModels/DashboardViewModel.cs b/FinTrack/ViewModels/DashboardViewModel.cs index fee4fec..7b6247b 100644 --- a/FinTrack/ViewModels/DashboardViewModel.cs +++ b/FinTrack/ViewModels/DashboardViewModel.cs @@ -1,93 +1,362 @@ using CommunityToolkit.Mvvm.ComponentModel; using FinTrackForWindows.Core; -using FinTrackForWindows.Dtos.AccountDtos; -using FinTrackForWindows.Dtos.BudgetDtos; -using FinTrackForWindows.Dtos.TransactionDtos; using FinTrackForWindows.Enums; +using FinTrackForWindows.Models.Account; +using FinTrackForWindows.Models.Budget; using FinTrackForWindows.Models.Dashboard; -using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Models.Debt; +using FinTrackForWindows.Models.Transaction; +using FinTrackForWindows.Services.Accounts; +using FinTrackForWindows.Services.Budgets; +using FinTrackForWindows.Services.Currencies; +using FinTrackForWindows.Services.Debts; +using FinTrackForWindows.Services.Memberships; +using FinTrackForWindows.Services.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Windows; using System.Windows.Media; +using System.Windows.Threading; namespace FinTrackForWindows.ViewModels { public partial class DashboardViewModel : ObservableObject { [ObservableProperty] - private ObservableCollection _budgets_DashboardView_ItemsControl; + private ObservableCollection? _budgets_DashboardView_ItemsControl; [ObservableProperty] - private ObservableCollection _currencyRates_DashboardView_ItemsControl; + private ObservableCollection? _currencyRates_DashboardView_ItemsControl; [ObservableProperty] - private ObservableCollection _accounts_DashboardView_ItemsControl; + private ObservableCollection? _accounts_DashboardView_ItemsControl; [ObservableProperty] - private string _totalBalance_DashboardView_TextBlock; + private string _totalBalance_DashboardView_TextBlock = string.Empty; [ObservableProperty] - private ObservableCollection _transactions_DashboardView_ListView; + private ObservableCollection? _transactions_DashboardView_ListView; [ObservableProperty] - private MembershipDashboard _currentMembership_DashboardView_Multiple; + private MembershipDashboard? _currentMembership_DashboardView_Multiple; [ObservableProperty] - private DebtDashboard _currentDebt_DashboardView_Multiple; + private DebtDashboard? _currentDebt_DashboardView_Multiple; [ObservableProperty] - private ObservableCollection _reports_DashboardView_ItemsControl; + private ObservableCollection? _reports_DashboardView_ItemsControl; [ObservableProperty] private string transactionSummary = string.Empty; + [ObservableProperty] + private DebtModel _currentActiveDebt; + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - private readonly IApiService _apiService; + private readonly IBudgetStore _budgetStore; + private readonly IAccountStore _accountStore; + private readonly ITransactionStore _transactionStore; + private readonly ICurrenciesStore _currenciesStore; + private readonly IMembershipStore _membershipStore; + private readonly IDebtStore _debtStore; + + public IEnumerable DashboardBudgets => _budgetStore.Budgets.Take(4); + public IEnumerable DashboardAccounts => _accountStore.Accounts.Take(2); + public IEnumerable DashboardTransactions => _transactionStore.Transactions.Take(50); + public IEnumerable DashboardDebts => _debtStore.MyDebtsList.Take(5); + + public event NotifyCollectionChangedEventHandler? BudgetsChanged; + public event NotifyCollectionChangedEventHandler? AccountsChanges; + public event NotifyCollectionChangedEventHandler? TransactionsChanges; + public event NotifyCollectionChangedEventHandler? MembershipChanges; + public event NotifyCollectionChangedEventHandler? DebtsChanges; + + private DispatcherTimer _debtCarouselTimer; + private int _currentDebtIndex = 0; public DashboardViewModel( ILogger logger, IServiceProvider serviceProvider, - IApiService apiService) + IBudgetStore budgetStore, + IAccountStore accountStore, + ITransactionStore transactionStore, + ICurrenciesStore currenciesStore, + IMembershipStore membershipStore, + IDebtStore debtStore) { _logger = logger; _serviceProvider = serviceProvider; - _apiService = apiService; + _budgetStore = budgetStore; + _accountStore = accountStore; + _transactionStore = transactionStore; + _currenciesStore = currenciesStore; + _membershipStore = membershipStore; + _debtStore = debtStore; + + _budgetStore.BudgetsChanged += OnBudgetsChanged; + _accountStore.AccountsChanged += OnAccountsChanged; + _transactionStore.TransactionsChanged += OnTransactionsChanged; + _membershipStore.CurrentUserMembershipChanged += OnMembershipChanged; + _debtStore.DebtsChanged += OnDebtsChanged; if (SessionManager.IsLoggedIn) { _logger.LogInformation("Kullanıcı zaten giriş yapmış. DashboardViewModel verileri yüklüyor."); - _ = LoadBudgetData(); - _ = LoadAccountData(); - _ = LoadTransactionData(); + _ = LoadInitialDataAsync(); } } - private async Task LoadData() + private async Task LoadInitialDataAsync() + { + await Task.WhenAll( + _budgetStore.LoadBudgetsAsync(), + _accountStore.LoadAccountsAsync(), + _transactionStore.LoadTransactionsAsync(), + _currenciesStore.LoadCurrenciesAsync(), + _membershipStore.LoadAllMembershipDataAsync(), + _debtStore.LoadDebtsAsync() + ); + + RefreshDashboardBudgets(); + RefreshDashboardAccounts(); + RefreshDashboardTransactions(); + RefreshDashboardCurrencies(); + RefreshDashboardMembership(); + RefreshDashboardDebts(); + + LoadReportData(); + + await CalculateTotalBalance(); + } + + private void RefreshDashboardBudgets() { - // [TEST] - CurrencyRates_DashboardView_ItemsControl = new ObservableCollection + Budgets_DashboardView_ItemsControl = new ObservableCollection(); + + foreach (var budget in DashboardBudgets) { - new CurrencyRateDashboard + Budgets_DashboardView_ItemsControl.Add(new BudgetDashboardModel { - FromCurrencyFlagUrl = "https://flagcdn.com/w320/us.png", FromCurrencyCountry = "Amerika Birleşik Devletleri", FromCurrencyName = "Dolar", FromCurrencyAmount = "1.00", - ToCurrencyFlagUrl = "https://flagcdn.com/w320/tr.png", ToCurrencyCountry = "Türkiye", ToCurrencyName = "Türk Lirası", ToCurrencyAmount = "27.50", ToCurrencyImageHeight = 20 - }, - new CurrencyRateDashboard + Name = budget.Name, + DueDate = budget.EndDate.ToString("dd.MM.yyyy"), + Amount = $"{budget.AllocatedAmount} {budget.Currency}", + RemainingTime = $"Son {(budget.EndDate - budget.StartDate).Days} gün kaldı.", + StatusBrush = (Brush)Application.Current.FindResource("StatusGreenBrush") + }); + } + } + + private void OnBudgetsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + App.Current.Dispatcher.Invoke(() => + { + _logger.LogInformation("BudgetStore değişti, Dashboard UI yenileniyor."); + RefreshDashboardBudgets(); + }); + } + + // ------ + + private void RefreshDashboardAccounts() + { + Accounts_DashboardView_ItemsControl = new ObservableCollection(); + + foreach (var account in DashboardAccounts) + { + Accounts_DashboardView_ItemsControl.Add(new AccountDashboard { - FromCurrencyFlagUrl = "https://flagcdn.com/w320/eu.png", FromCurrencyCountry = "Euro Bölgesi", FromCurrencyName = "Euro",FromCurrencyAmount = "1.00", - ToCurrencyFlagUrl = "https://flagcdn.com/w320/tr.png", ToCurrencyCountry = "Türkiye", ToCurrencyName = "Türk Lirası", ToCurrencyAmount = "30.00", ToCurrencyImageHeight = 20 + Name = account.Name, + Percentage = 70, + Balance = "Test", + ProgressBarBrush = (Brush)Application.Current.FindResource("StatusGreenBrush") + }); + } + } + + private void OnAccountsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + App.Current.Dispatcher.Invoke(() => + { + _logger.LogInformation("AccountStore değişti, Dashboard UI yenileniyor."); + RefreshDashboardAccounts(); + }); + + _ = CalculateTotalBalance(); + } + + private async Task CalculateTotalBalance() + { + if (_accountStore.Accounts == null || !_accountStore.Accounts.Any()) + { + TotalBalance_DashboardView_TextBlock = "0.00 TRY"; + return; + } + + string targetCurrency = "TRY"; + decimal totalBalanceInTargetCurrency = 0; + + var conversionTasks = new List>(); + + foreach (var account in _accountStore.Accounts) + { + decimal balance = account.Balance ?? 0; + string accountCurrency = account.Currency.ToString(); + + if (balance == 0) continue; + + if (accountCurrency == targetCurrency) + { + totalBalanceInTargetCurrency += balance; + } + else + { + conversionTasks.Add( + _currenciesStore.GetConvertCurrencies(accountCurrency, targetCurrency, balance) + ); } + } + var convertedAmounts = await Task.WhenAll(conversionTasks); + + totalBalanceInTargetCurrency += convertedAmounts.Sum(); + + TotalBalance_DashboardView_TextBlock = $"{totalBalanceInTargetCurrency:N2} {targetCurrency}"; + } + + // ------ + + private void RefreshDashboardTransactions() + { + Transactions_DashboardView_ListView = new ObservableCollection(); + + foreach (var transaction in DashboardTransactions) + { + Transactions_DashboardView_ListView.Add(new TransactionDashboard + { + DateText = transaction.Date.ToString("dd.MM.yyyy"), + Description = transaction.NameOrDescription ?? "N/A", + Amount = $"{transaction.Amount} {transaction.Currency}", + Category = transaction.CategoryName, + Type = transaction.Type, + }); + } + + double totalIncome = Transactions_DashboardView_ListView + .Where(t => t.Type == TransactionType.Income) + .Sum(t => + { + var cleaned = t.Amount.Replace("+", string.Empty).Replace("$", string.Empty).Trim(); + return double.TryParse(cleaned, out var value) ? value : 0; + }); + double totalExpense = Transactions_DashboardView_ListView + .Where(t => t.Type == TransactionType.Expense) + .Sum(t => + { + var cleaned = t.Amount.Replace("-", string.Empty).Replace("$", string.Empty).Trim(); + return double.TryParse(cleaned, out var value) ? value : 0; + }); + double remainingBalance = totalIncome - totalExpense; + + TransactionSummary = + $"Toplam {Transactions_DashboardView_ListView.Count} işlem bulundu. " + + $"Gelir: +{totalIncome} {Transactions_DashboardView_ListView.FirstOrDefault()?.Amount?.Split(' ')?.LastOrDefault() ?? ""}, " + + $"Gider: -{totalExpense} {Transactions_DashboardView_ListView.FirstOrDefault()?.Amount?.Split(' ')?.LastOrDefault() ?? ""} " + + $"Kalan: {remainingBalance} {Transactions_DashboardView_ListView.FirstOrDefault()?.Amount?.Split(' ')?.LastOrDefault() ?? ""}"; + } + + private void OnTransactionsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + App.Current.Dispatcher.Invoke(() => + { + _logger.LogInformation("TransactionsStore değişti, Dashboard UI yenileniyor."); + RefreshDashboardTransactions(); + }); + } + + // ------ + + private void RefreshDashboardCurrencies() + { + CurrencyRates_DashboardView_ItemsControl = new ObservableCollection(); + + var tryRate = _currenciesStore.Currencies.FirstOrDefault(c => c.ToCurrencyCode == "TRY"); + var eurRate = _currenciesStore.Currencies.FirstOrDefault(c => c.ToCurrencyCode == "EUR"); + + if (tryRate != null) + { + CurrencyRates_DashboardView_ItemsControl.Add(new CurrencyRateDashboard + { + FromCurrencyName = "USD", + ToCurrencyName = tryRate.ToCurrencyCode, + ToCurrencyAmount = tryRate.ToCurrencyPrice.ToString("N4"), + FromCurrencyFlagUrl = "https://flagcdn.com/w320/us.png", + ToCurrencyFlagUrl = tryRate.ToCurrencyFlag, + FromCurrencyAmount = "1.00", + ToCurrencyCountry = tryRate.ToCurrencyName, + FromCurrencyCountry = "United States", + ToCurrencyImageHeight = 20 + }); + } + + if (eurRate != null) + { + CurrencyRates_DashboardView_ItemsControl.Add(new CurrencyRateDashboard + { + FromCurrencyName = "USD", + ToCurrencyName = eurRate.ToCurrencyCode, + ToCurrencyAmount = eurRate.ToCurrencyPrice.ToString("N4"), + FromCurrencyFlagUrl = "https://flagcdn.com/w320/us.png", + ToCurrencyFlagUrl = eurRate.ToCurrencyFlag, + FromCurrencyAmount = "1.00", + ToCurrencyCountry = eurRate.ToCurrencyName, + FromCurrencyCountry = "Euro Area", + ToCurrencyImageHeight = 20 + }); + } + } + + // ------ + + private void RefreshDashboardMembership() + { + var currentMembership = _membershipStore.CurrentUserMembership; + + if (currentMembership == null) + { + CurrentMembership_DashboardView_Multiple = null; + return; + } + + var correspondingPlan = _membershipStore.AvailablePlans.FirstOrDefault(p => p.Id == currentMembership.PlanId); + + string price = correspondingPlan?.Price.ToString() ?? "N/A" + " " + correspondingPlan?.Currency; + + CurrentMembership_DashboardView_Multiple = new MembershipDashboard + { + Level = $"{currentMembership.PlanName} | {currentMembership.Status.ToUpper()}", + StartDate = currentMembership.StartDate.ToString("dd.MM.yyyy"), + RenewalDate = currentMembership.EndDate.ToString("dd.MM.yyyy"), + Price = price, }; + } - // [TEST] - CurrentMembership_DashboardView_Multiple = new MembershipDashboard { Level = "Pro | AKTF", StartDate = "01.01.2025", RenewalDate = "01.02.2025", Price = "9.99$" }; + private void OnMembershipChanged() + { + App.Current.Dispatcher.Invoke(() => + { + _logger.LogInformation("Membershipstore değişti, Dashboard UI yenileniyor."); + RefreshDashboardMembership(); + }); + } - // [TEST] + // ------ + + private void RefreshDashboardDebts() + { CurrentDebt_DashboardView_Multiple = new DebtDashboard { LenderName = "Ali Veli", @@ -101,107 +370,79 @@ private async Task LoadData() DueDate = "01.02.2025", ReviewDate = "01.03.2025" }; - - // [TEST] - Reports_DashboardView_ItemsControl = new ObservableCollection - { - CreateReport("2025 Yılı Finansal Raporu"), - CreateReport("2024 Yılı Tasarruf Raporu"), - CreateReport("2023 Yılı Gelir-Gider Raporu"), - CreateReport("2022 Yılı Bütçe Raporu") - }; } - private ReportDashboardModel CreateReport(string name) + private void OnDebtsChanged() { - var reportLogger = _serviceProvider.GetRequiredService>(); - var report = new ReportDashboardModel(reportLogger) + App.Current.Dispatcher.Invoke(() => { - Name = name, - }; - return report; + _logger.LogInformation("DebtStore değişti, Dashboard borçları yenileniyor."); + SetupDebtCarousel(); + }); } - private async Task LoadBudgetData() + private void SetupDebtCarousel() { - var budgets = await _apiService.GetAsync>("Budgets"); - Budgets_DashboardView_ItemsControl = new ObservableCollection(); - if (budgets != null) + _debtCarouselTimer?.Stop(); + + if (_debtStore.ActiveDebts != null && _debtStore.ActiveDebts.Any()) { - for (int i = 0; i < 4; i++) + _currentDebtIndex = 0; + CurrentActiveDebt = _debtStore.ActiveDebts[_currentDebtIndex]; + + if (_debtStore.ActiveDebts.Count > 1) { - Budgets_DashboardView_ItemsControl.Add(new BudgetDashboardModel - { - Name = budgets[i].Name, - DueDate = budgets[i].EndDate.ToString("dd.MM.yyyy"), - Amount = $"{budgets[i].AllocatedAmount} {budgets[i].Currency}", - RemainingTime = $"Son {(budgets[i].EndDate - budgets[i].StartDate).Days} gün kaldı.", - StatusBrush = (Brush)Application.Current.FindResource("StatusGreenBrush") - }); + _debtCarouselTimer = new DispatcherTimer(); + _debtCarouselTimer.Interval = TimeSpan.FromSeconds(2); + _debtCarouselTimer.Tick += DebtCarouselTimer_Tick; + _debtCarouselTimer.Start(); } } + else + { + CurrentActiveDebt = null; + } } - private async Task LoadAccountData() + private void DebtCarouselTimer_Tick(object sender, EventArgs e) { - var accounts = await _apiService.GetAsync>("Account"); - Accounts_DashboardView_ItemsControl = new ObservableCollection(); - if (accounts != null) + if (_debtStore.ActiveDebts == null || _debtStore.ActiveDebts.Count == 0) { - decimal totalBalance = 0; - for (int i = 0; i < 2; i++) - { - Accounts_DashboardView_ItemsControl.Add(new AccountDashboard - { - Name = accounts[i].Name, - Balance = $"{accounts[i].Balance} {accounts[i].Currency}", - Percentage = 70, // Burası yüzdelik bar olduğu için daha iyi bir hesaplama yapılmalı. - ProgressBarBrush = (Brush)Application.Current.FindResource("StatusGreenBrush") - }); - - totalBalance += accounts[i].Balance; - } + _debtCarouselTimer.Stop(); + CurrentActiveDebt = null; + return; + } - TotalBalance_DashboardView_TextBlock = $"{totalBalance} {accounts[0].Currency}"; + _currentDebtIndex++; + if (_currentDebtIndex >= _debtStore.ActiveDebts.Count) + { + _currentDebtIndex = 0; } + + CurrentActiveDebt = _debtStore.ActiveDebts[_currentDebtIndex]; } - private async Task LoadTransactionData() + // ------ + + private void LoadReportData() { - var transactions = await _apiService.GetAsync>("Transactions"); - Transactions_DashboardView_ListView = new ObservableCollection(); - if (transactions != null) + Reports_DashboardView_ItemsControl = new ObservableCollection { - foreach (var item in transactions) - { - Transactions_DashboardView_ListView.Add(new TransactionDashboard - { - DateText = item.TransactionDateUtc.ToString("dd.MM.yyyy"), - Description = item.Description ?? "N/A", - Amount = $"{item.Amount} {item.Currency}", - Category = item.Category.Name, - Type = item.Category.Type - }); - } + CreateReport("2025 Yılı Finansal Raporu"), + CreateReport("2024 Yılı Tasarruf Raporu"), + CreateReport("2023 Yılı Gelir-Gider Raporu"), + CreateReport("2022 Yılı Bütçe Raporu") + }; + } - double totalIncome = Transactions_DashboardView_ListView - .Where(t => t.Type == TransactionType.Income) - .Sum(t => - { - var cleaned = t.Amount.Replace("+", string.Empty).Replace("$", string.Empty).Trim(); - return double.TryParse(cleaned, out var value) ? value : 0; - }); - double totalExpense = Transactions_DashboardView_ListView - .Where(t => t.Type == TransactionType.Expense) - .Sum(t => - { - var cleaned = t.Amount.Replace("-", string.Empty).Replace("$", string.Empty).Trim(); - return double.TryParse(cleaned, out var value) ? value : 0; - }); - double remainingBalance = totalIncome - totalExpense; - - TransactionSummary = $"Toplam {Transactions_DashboardView_ListView.Count} işlem bulundu. Gelir: +{totalIncome} {transactions[0].Currency}, Gider: -{totalExpense} {transactions[0].Currency} Kalan: {remainingBalance} {transactions[0].Currency}"; - } + private ReportDashboardModel CreateReport(string name) + { + var reportLogger = _serviceProvider.GetRequiredService>(); + var report = new ReportDashboardModel(reportLogger) + { + Name = name, + }; + return report; } } } diff --git a/FinTrack/ViewModels/DebtViewModel.cs b/FinTrack/ViewModels/DebtViewModel.cs index b64861c..e786929 100644 --- a/FinTrack/ViewModels/DebtViewModel.cs +++ b/FinTrack/ViewModels/DebtViewModel.cs @@ -1,15 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using FinTrackForWindows.Core; -using FinTrackForWindows.Dtos.DebtDtos; -using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Debt; -using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.Debts; using Microsoft.Extensions.Logging; using Microsoft.Win32; -using System.Collections.ObjectModel; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; using System.Windows; namespace FinTrackForWindows.ViewModels @@ -17,12 +11,9 @@ namespace FinTrackForWindows.ViewModels public partial class DebtViewModel : ObservableObject { private readonly ILogger _logger; - private readonly IApiService _apiService; + private readonly IDebtStore _debtStore; - private readonly int _currentUserId; - - [ObservableProperty] - private bool isLoading; + public IDebtStore DebtStore => _debtStore; [ObservableProperty] private string? newProposalBorrowerEmail; @@ -36,120 +27,36 @@ public partial class DebtViewModel : ObservableObject [ObservableProperty] private DateTime newProposalDueDate = DateTime.Now.AddMonths(1); - [ObservableProperty] - private ObservableCollection pendingOffers; - - [ObservableProperty] - private ObservableCollection myDebtsList; - - public DebtViewModel(ILogger logger, IApiService apiService) + public DebtViewModel(ILogger logger, IDebtStore debtStore) { _logger = logger; - _apiService = apiService; - - var handler = new JwtSecurityTokenHandler(); - var jsonToken = handler.ReadJwtToken(SessionManager.CurrentToken); - _currentUserId = Convert.ToInt16(jsonToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value); - - pendingOffers = new ObservableCollection(); - myDebtsList = new ObservableCollection(); - - _ = LoadDebtsAsync(); + _debtStore = debtStore; } [RelayCommand] - private async Task LoadDebtsAsync() + private async Task SendOfferAsync() { - IsLoading = true; - _logger.LogInformation("Loading debt data from API..."); - try + if (string.IsNullOrWhiteSpace(NewProposalBorrowerEmail) || NewProposalAmount <= 0) { - var debtDtos = await _apiService.GetAsync>("Debt"); - if (debtDtos == null) return; - - PendingOffers.Clear(); - MyDebtsList.Clear(); - - foreach (var dto in debtDtos) - { - var debtModel = new DebtModel - { - Id = dto.Id, - LenderId = dto.LenderId, - BorrowerId = dto.BorrowerId, - CurrentUserId = _currentUserId, - LenderName = dto.LenderName, - BorrowerName = dto.BorrowerName, - borrowerImageUrl = dto.BorrowerProfilePicture ?? "/Assets/Images/Icons/user-red.png", - lenderImageUrl = dto.LenderProfilePicture ?? "/Assets/Images/Icons/user-green.png", - Amount = dto.Amount, - DueDate = dto.DueDateUtc.ToLocalTime(), - Status = dto.Status - }; - - // Bana gelen ve henüz karar vermediğim borç teklifleri - if (dto.Status == DebtStatusType.PendingBorrowerAcceptance && dto.BorrowerId == _currentUserId) - { - PendingOffers.Add(debtModel); - } - else // Diğer tüm borçlarım (aktif, ödenmiş, reddedilmiş vb.) - { - MyDebtsList.Add(debtModel); - } - } - _logger.LogInformation("Successfully loaded and processed {Count} debts.", debtDtos.Count); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to load debt data."); - MessageBox.Show("Borç verileri yüklenirken bir hata oluştu.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + MessageBox.Show("Lütfen borçlu e-postası ve geçerli bir miktar girin.", "Doğrulama Hatası", MessageBoxButton.OK, MessageBoxImage.Warning); + return; } - finally - { - IsLoading = false; - } - } - [RelayCommand] - private async Task SendOfferAsync() - { - IsLoading = true; try { - if (string.IsNullOrWhiteSpace(NewProposalBorrowerEmail) || NewProposalAmount <= 0) - { - _logger.LogWarning("New proposal validation failed: Borrower email or amount is invalid."); - return; - } + await _debtStore.SendOfferAsync(NewProposalBorrowerEmail, NewProposalAmount, "TRY", NewProposalDueDate, NewProposalDescription); - var createDto = new CreateDebtOfferRequestDto - { - BorrowerEmail = NewProposalBorrowerEmail, - Amount = NewProposalAmount, - CurrencyCode = BaseCurrencyType.TRY, - DueDateUtc = NewProposalDueDate, - Description = NewProposalDescription - }; - - _logger.LogInformation("Sending new debt offer to API..."); - var result = await _apiService.PostAsync("Debt/create-debt-offer", createDto); + NewProposalBorrowerEmail = string.Empty; + NewProposalAmount = 0; + NewProposalDescription = string.Empty; + NewProposalDueDate = DateTime.Now.AddMonths(1); - if (result != null) - { - NewProposalBorrowerEmail = string.Empty; - NewProposalAmount = 0; - await LoadDebtsAsync(); - - _logger.LogInformation("Debt offer sent successfully."); - } + MessageBox.Show("Teklif başarıyla gönderildi.", "Başarılı", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { - _logger.LogError(ex, "Failed to send debt offer."); - } - finally - { - IsLoading = false; + _logger.LogError(ex, "Failed to send debt offer from ViewModel."); + MessageBox.Show("Teklif gönderilirken bir hata oluştu.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -158,27 +65,14 @@ private async Task RespondToOfferAsync(object parameter) { if (parameter is not object[] values || values.Length != 2 || values[0] is not DebtModel debt || values[1] is not bool decision) return; - IsLoading = true; try { - _logger.LogInformation("Attempting to {Action} offer for DebtId: {DebtId}", decision, debt.Id); - - var requestBody = new { Accepted = decision }; - bool result = await _apiService.PostAsync($"Debt/respond-to-offer/{debt.Id}", requestBody); - - if (result) - { - _logger.LogInformation("Successfully responded to offer for DebtId: {DebtId}", debt.Id); - await LoadDebtsAsync(); - } + await _debtStore.RespondToOfferAsync(debt, decision); } catch (Exception ex) { - _logger.LogError(ex, "Failed to respond to debt offer for DebtId: {DebtId}", debt.Id); - } - finally - { - IsLoading = false; + _logger.LogError(ex, "Failed to respond to offer from ViewModel for DebtId: {DebtId}", debt.Id); + MessageBox.Show("Teklife yanıt verilirken bir hata oluştu.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -189,31 +83,21 @@ private async Task UploadVideoAsync(DebtModel debt) OpenFileDialog openFileDialog = new OpenFileDialog { - Filter = "Video Files (*.mp4;*.mov;*.wmv)|*.mp4;*.mov;*.wmv|All files (*.*)|*.*", + Filter = "Video Files (*.mp4;*.mov;*.wmv)|*.mp4;*.mov;*.wmv", Title = "Select an Approval Video" }; if (openFileDialog.ShowDialog() == true) { - IsLoading = true; try { - _logger.LogInformation("Uploading video for DebtId: {DebtId}", debt.Id); - var success = await _apiService.UploadFileAsync($"Videos/user-upload-video?debtId={debt.Id}", openFileDialog.FileName); - - if (success) - { - _logger.LogInformation("Video uploaded successfully for DebtId: {DebtId}", debt.Id); - await LoadDebtsAsync(); - } + await _debtStore.UploadVideoAsync(debt, openFileDialog.FileName); + MessageBox.Show("Video başarıyla yüklendi.", "Başarılı", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { - _logger.LogError(ex, "Failed to upload video for DebtId: {DebtId}", debt.Id); - } - finally - { - IsLoading = false; + _logger.LogError(ex, "Failed to upload video from ViewModel for DebtId: {DebtId}", debt.Id); + MessageBox.Show("Video yüklenirken bir hata oluştu.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); } } } diff --git a/FinTrack/ViewModels/SettingsViewModel.cs b/FinTrack/ViewModels/SettingsViewModel.cs index 4c078c4..0f7f455 100644 --- a/FinTrack/ViewModels/SettingsViewModel.cs +++ b/FinTrack/ViewModels/SettingsViewModel.cs @@ -1,113 +1,104 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.MembershipDtos; +using FinTrackForWindows.Services.Memberships; using Microsoft.Extensions.Logging; +using System.Diagnostics; using System.Windows; -using System.Windows.Input; namespace FinTrackForWindows.ViewModels { public partial class SettingsViewModel : ObservableObject { - [ObservableProperty] - private string freeButtonStatus = string.Empty; - - [ObservableProperty] - private string plusButtonStatus = string.Empty; - - [ObservableProperty] - private string proButtonStatus = string.Empty; - - [ObservableProperty] - private bool isFreeButtonEnabled = false; - - [ObservableProperty] - private bool isPlusButtonEnabled = true; - - [ObservableProperty] - private bool isProButtonEnabled = true; - private readonly ILogger _logger; + private readonly IMembershipStore _membershipStore; - private readonly ProfileSettingsContentViewModel profileVM; - private readonly SecuritySettingsContentViewModel securityVM; - private readonly NotificationSettingsContentViewModel notificationsVM; - private readonly AppSettingsContentViewModel appVM; + private readonly ProfileSettingsContentViewModel _profileVM; + private readonly SecuritySettingsContentViewModel _securityVM; + private readonly NotificationSettingsContentViewModel _notificationsVM; + private readonly AppSettingsContentViewModel _appVM; [ObservableProperty] - private ObservableObject currentPageViewModel; + private ObservableObject _currentPageViewModel; - public ICommand ChangePageCommand { get; } + public IMembershipStore MembershipStore => _membershipStore; public SettingsViewModel( ILogger logger, + IMembershipStore membershipStore, ProfileSettingsContentViewModel profileVM, SecuritySettingsContentViewModel securityVM, NotificationSettingsContentViewModel notificationsVM, - AppSettingsContentViewModel appVM - ) + AppSettingsContentViewModel appVM) { _logger = logger; - InitializeButtonStatuses(); - this.profileVM = profileVM; - this.securityVM = securityVM; - this.notificationsVM = notificationsVM; - this.appVM = appVM; - - ChangePageCommand = new RelayCommand(ChangePage); + _membershipStore = membershipStore; + _profileVM = profileVM; + _securityVM = securityVM; + _notificationsVM = notificationsVM; + _appVM = appVM; - CurrentPageViewModel = profileVM; - } + _currentPageViewModel = _profileVM; - private void ChangePage(string pageName) - { - _logger.LogInformation($"Sayfa değiştiriliyor: {pageName}"); - switch (pageName) - { - case "Profile": - CurrentPageViewModel = profileVM; - break; - case "Security": - CurrentPageViewModel = securityVM; - break; - case "Notifications": - CurrentPageViewModel = notificationsVM; - break; - case "App": - CurrentPageViewModel = appVM; - break; - default: - _logger.LogWarning($"Bilinmeyen sayfa adı: {pageName}"); - break; - } + _ = LoadInitialData(); } - private void InitializeButtonStatuses() + private async Task LoadInitialData() { - FreeButtonStatus = "Seçili"; - PlusButtonStatus = "Yükselt"; - ProButtonStatus = "Yükselt"; - _logger.LogInformation("Üyelik düğmelerinin durumları ayarlandı."); + await _membershipStore.LoadAllMembershipDataAsync(); } [RelayCommand] - private void SelectFreeMembership() + private void ChangePage(string pageName) { - _logger.LogInformation("Free üyelik seçildi."); - MessageBox.Show("Free üyelik seçildi.", "Üyelik Seçimi", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation($"Changing settings page to: {pageName}"); + CurrentPageViewModel = pageName switch + { + "Profile" => _profileVM, + "Security" => _securityVM, + "Notifications" => _notificationsVM, + "App" => _appVM, + _ => CurrentPageViewModel + }; } [RelayCommand] - private void SelectPlusMembership() + private async Task SelectPlan(PlanFeatureDto selectedPlan) { - _logger.LogInformation("Plus üyelik seçildi."); - MessageBox.Show("Plus üyelik seçildi.", "Üyelik Seçimi", MessageBoxButton.OK, MessageBoxImage.Information); - } + if (selectedPlan == null) + { + _logger.LogWarning("SelectPlan command executed with a null parameter."); + return; + } - [RelayCommand] - private void SelectProMembership() - { - _logger.LogInformation("Pro üyelik seçildi."); - MessageBox.Show("Pro üyelik seçildi.", "Üyelik Seçimi", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation($"Plan selection initiated for: {selectedPlan.Name} (ID: {selectedPlan.Id})"); + + if (MembershipStore.CurrentUserMembership != null && MembershipStore.CurrentUserMembership.PlanId == selectedPlan.Id) + { + MessageBox.Show("This is already your current plan.", "Information", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + string checkoutUrl = await _membershipStore.SelectPlanAsync(selectedPlan.Id, true); + + if (!string.IsNullOrEmpty(checkoutUrl)) + { + _logger.LogInformation($"Redirecting user to checkout URL: {checkoutUrl}"); + try + { + Process.Start(new ProcessStartInfo(checkoutUrl) { UseShellExecute = true }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to open web browser for checkout."); + MessageBox.Show($"Could not open the payment page. Please copy this link and open it manually:\n\n{checkoutUrl}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + else + { + _logger.LogInformation("No checkout URL received, assuming successful subscription to a free plan or an API error occurred."); + MessageBox.Show("Your subscription status has been updated. Please check your profile.", "Subscription Update", MessageBoxButton.OK, MessageBoxImage.Information); + } } } -} +} \ No newline at end of file diff --git a/FinTrack/ViewModels/TransactionsViewModel.cs b/FinTrack/ViewModels/TransactionsViewModel.cs index a614700..f868cda 100644 --- a/FinTrack/ViewModels/TransactionsViewModel.cs +++ b/FinTrack/ViewModels/TransactionsViewModel.cs @@ -2,8 +2,12 @@ using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Dtos.TransactionDtos; using FinTrackForWindows.Enums; +using FinTrackForWindows.Models.Account; using FinTrackForWindows.Models.Transaction; +using FinTrackForWindows.Services.Accounts; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.Transactions; +using FinTrackForWindows.Views; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -15,9 +19,14 @@ public partial class TransactionsViewModel : ObservableObject { private readonly ILogger _logger; private readonly IApiService _apiService; + private readonly ITransactionStore _transactionStore; + private readonly IAccountStore _accountStore; + + public ReadOnlyObservableCollection Transactions => _transactionStore.Transactions; + public ReadOnlyObservableCollection AllAccounts => _accountStore.Accounts; [ObservableProperty] - private ObservableCollection transactions; + private ObservableCollection allCategories; [ObservableProperty] private TransactionModel editableTransaction; @@ -25,6 +34,25 @@ public partial class TransactionsViewModel : ObservableObject [ObservableProperty] private TransactionModel? selectedTransaction; + [ObservableProperty] + private AccountModel? selectedAccountForForm; + + public ObservableCollection CurrencyTypes { get; } = new(); + + private TransactionCategoryModel? _selectedCategoryForForm; + public TransactionCategoryModel? SelectedCategoryForForm + { + get => _selectedCategoryForForm; + set + { + SetProperty(ref _selectedCategoryForForm, value); + if (value != null && EditableTransaction != null) + { + EditableTransaction.Type = value.Type; + } + } + } + [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveTransactionCommand))] private bool isEditing; @@ -41,12 +69,24 @@ public partial class TransactionsViewModel : ObservableObject [ObservableProperty] private string mostSpending; - public TransactionsViewModel(ILogger logger, IApiService apiService) + public TransactionsViewModel(ILogger logger, IApiService apiService, + ITransactionStore transactionStore, IAccountStore accountStore) { _logger = logger; _apiService = apiService; - _ = LoadData(); + _transactionStore = transactionStore; + _accountStore = accountStore; + + AllCategories = new ObservableCollection(); + foreach (BaseCurrencyType currencyType in Enum.GetValues(typeof(BaseCurrencyType))) + { + CurrencyTypes.Add(currencyType); + } + + _transactionStore.TransactionsChanged += OnTransactionsChanged; + + _ = LoadData(); NewTransaction(); } @@ -54,7 +94,10 @@ public TransactionsViewModel(ILogger logger, IApiService private void NewTransaction() { IsEditing = false; - EditableTransaction = new TransactionModel(); + EditableTransaction = new TransactionModel { Date = DateTime.Now }; + SelectedCategoryForForm = null; + SelectedAccountForForm = null; + _logger.LogInformation("Yeni işlem formu hazırlandı."); OnPropertyChanged(nameof(FormTitle)); @@ -62,7 +105,7 @@ private void NewTransaction() } [RelayCommand] - private void EditTransaction(TransactionModel transactionToEdit) + private void PrepareToEditTransaction(TransactionModel transactionToEdit) { if (transactionToEdit == null) return; @@ -71,122 +114,137 @@ private void EditTransaction(TransactionModel transactionToEdit) EditableTransaction = new TransactionModel() { Id = transactionToEdit.Id, + AccountId = transactionToEdit.AccountId, NameOrDescription = transactionToEdit.NameOrDescription, Amount = transactionToEdit.Amount, Type = transactionToEdit.Type, Date = transactionToEdit.Date, AccountName = transactionToEdit.AccountName, - Category = transactionToEdit.Category, + CategoryId = transactionToEdit.CategoryId, + CategoryName = transactionToEdit.CategoryName, Currency = transactionToEdit.Currency }; - _logger.LogInformation("'{TransactionName}' adlı işlem düzenleniyor.", EditableTransaction.NameOrDescription); + SelectedCategoryForForm = AllCategories.FirstOrDefault(c => c.Id == transactionToEdit.CategoryId); + SelectedAccountForForm = AllAccounts.FirstOrDefault(a => a.Id == transactionToEdit.AccountId); + + _logger.LogInformation("'{TransactionName}' adlı işlem düzenlenmek üzere forma yüklendi.", EditableTransaction.NameOrDescription); OnPropertyChanged(nameof(FormTitle)); OnPropertyChanged(nameof(SaveButtonText)); } - [RelayCommand] - private async Task DeleteTransaction(TransactionModel transactionToDelete) + private async Task DeleteTransaction(TransactionModel? transactionToDelete) { - if (transactionToDelete != null) - { - await _apiService.DeleteAsync($"Transactions/{transactionToDelete.Id}"); - Transactions.Remove(transactionToDelete); - _logger.LogInformation("'{TransactionName}' adlı işlem silindi.", transactionToDelete.NameOrDescription); - NewTransaction(); - } + if (transactionToDelete == null) return; + + var result = MessageBox.Show($"'{transactionToDelete.NameOrDescription}' işlemini silmek istediğinizden emin misiniz?", "Silme Onayı", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.No) return; + + await _transactionStore.DeleteTransactionAsync(transactionToDelete.Id); + NewTransaction(); } [RelayCommand] private async Task SaveTransaction() { - if (string.IsNullOrWhiteSpace(EditableTransaction.NameOrDescription)) + if (string.IsNullOrWhiteSpace(EditableTransaction.NameOrDescription) || + SelectedAccountForForm == null || + SelectedCategoryForForm == null) { - _logger.LogWarning("Kaydetme denemesi başarısız: İşlem açıklaması boş."); - MessageBox.Show("İşlem açıklaması boş olamaz.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + MessageBox.Show("Lütfen tüm zorunlu alanları (açıklama, hesap, kategori) doldurun.", "Eksik Bilgi", MessageBoxButton.OK, MessageBoxImage.Warning); return; } + var transactionDto = new TransactionCreateDto + { + Description = EditableTransaction.NameOrDescription, + Amount = EditableTransaction.Amount, + TransactionDateUtc = EditableTransaction.Date.ToUniversalTime(), + AccountId = SelectedAccountForForm?.Id ?? -1, + CategoryId = SelectedCategoryForForm.Id, + Currency = EditableTransaction.Currency + }; + if (IsEditing) { - var existing = Transactions.FirstOrDefault(t => t.Id == EditableTransaction.Id); - if (existing != null) - { - existing.NameOrDescription = EditableTransaction.NameOrDescription; - existing.Amount = EditableTransaction.Amount; - existing.Type = EditableTransaction.Type; - existing.Date = EditableTransaction.Date; - existing.AccountName = EditableTransaction.AccountName; - existing.Category = EditableTransaction.Category; - existing.Currency = EditableTransaction.Currency; - - // TODO: Yeni Gelir/Gider şemasına uygun. - //await _apiService.PutAsync($"Transactions/{existing.Id}", new TransactionUpdateDto - //{ - - //}); - - _logger.LogInformation("'{TransactionName}' adlı işlem güncellendi.", existing.NameOrDescription); - CalculateTotals(); - } + await _transactionStore.UpdateTransactionAsync(EditableTransaction.Id, transactionDto); + _logger.LogInformation("'{TransactionName}' adlı işlem güncellendi.", EditableTransaction.NameOrDescription); } else { - Transactions.Add(EditableTransaction); + await _transactionStore.AddTransactionAsync(transactionDto); _logger.LogInformation("Yeni işlem eklendi: '{TransactionName}'", EditableTransaction.NameOrDescription); } NewTransaction(); } - private async Task LoadData() + [RelayCommand] + private async Task AddNewCategory() { - var transactions = await _apiService.GetAsync>("Transactions"); - Transactions = new ObservableCollection(); - if (transactions != null) + var addCategoryWindow = new AddCategoryWindow(_apiService); + + if (addCategoryWindow.ShowDialog() == true) { - foreach (var transaction in transactions) + _logger.LogInformation("Yeni kategori başarıyla eklendi, liste yenileniyor."); + await LoadCategories(); + + var newCategory = addCategoryWindow.GetCreatedCategory(); + if (newCategory != null) { - Transactions.Add(new TransactionModel - { - Id = transaction.Id, - NameOrDescription = transaction.Description ?? "N/A", - Amount = transaction.Amount, - Date = transaction.TransactionDateUtc, - AccountName = transaction.Account.Name, - Category = transaction.Category.Name, - Type = transaction.Category.Type, - Currency = transaction.Currency.ToString() - }); + SelectedCategoryForForm = AllCategories.FirstOrDefault(c => c.Id == newCategory.Id); } } + } - Transactions.CollectionChanged += OnTransactionsChanged; - CalculateTotals(); + private async Task LoadData() + { + await _accountStore.LoadAccountsAsync(); + await LoadCategories(); + await _transactionStore.LoadTransactionsAsync(); + } + + private async Task LoadCategories() + { + var categoryDtos = await _apiService.GetAsync>("TransactionCategory"); + + AllCategories.Clear(); + if (categoryDtos != null) + { + foreach (var dto in categoryDtos) + { + AllCategories.Add(new TransactionCategoryModel { Id = dto.Id, Name = dto.Name, Type = dto.Type }); + } + } + _logger.LogInformation("{Count} adet kategori yüklendi.", AllCategories.Count); } private void OnTransactionsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - _logger.LogInformation("Koleksiyon değişti, toplamlar yeniden hesaplanıyor."); - CalculateTotals(); + _logger.LogInformation("İşlem listesi değişti, toplamlar yeniden hesaplanıyor."); + App.Current.Dispatcher.Invoke(CalculateTotals); } private void CalculateTotals() { + if (Transactions == null) return; + TotalIncome = Transactions .Where(t => t.Type == TransactionType.Income) .Sum(t => t.Amount).ToString("C"); + TotalExpense = Transactions .Where(t => t.Type == TransactionType.Expense) .Sum(t => t.Amount).ToString("C"); + var mostSpendingTransaction = Transactions + .Where(t => t.Type == TransactionType.Expense) .OrderByDescending(t => t.Amount) .FirstOrDefault(); - MostSpending = mostSpendingTransaction != null - ? mostSpendingTransaction.Category - : "Henüz işlem yok"; + + MostSpending = mostSpendingTransaction?.CategoryName ?? "Henüz işlem yok"; } } } \ No newline at end of file diff --git a/FinTrack/Views/AddCategoryWindow.xaml b/FinTrack/Views/AddCategoryWindow.xaml new file mode 100644 index 0000000..31493a5 --- /dev/null +++ b/FinTrack/Views/AddCategoryWindow.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + @@ -83,7 +151,7 @@ @@ -119,22 +187,50 @@ + + + - + + + + + + + - - - - - + + + + + + - - + - - + +