diff --git a/FinTrack/App.xaml b/FinTrack/App.xaml index 8af4b5e..6360a8d 100644 --- a/FinTrack/App.xaml +++ b/FinTrack/App.xaml @@ -1,7 +1,8 @@  + xmlns:local="clr-namespace:FinTrackForWindows" + ShutdownMode="OnExplicitShutdown"> diff --git a/FinTrack/App.xaml.cs b/FinTrack/App.xaml.cs index 42738a5..c890ba3 100644 --- a/FinTrack/App.xaml.cs +++ b/FinTrack/App.xaml.cs @@ -3,12 +3,15 @@ using FinTrackForWindows.Services; using FinTrackForWindows.Services.Accounts; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; using FinTrackForWindows.Services.Budgets; using FinTrackForWindows.Services.Camera; using FinTrackForWindows.Services.Currencies; using FinTrackForWindows.Services.Debts; using FinTrackForWindows.Services.Memberships; +using FinTrackForWindows.Services.Reports; using FinTrackForWindows.Services.Transactions; +using FinTrackForWindows.Services.Users; using FinTrackForWindows.ViewModels; using FinTrackForWindows.Views; using Microsoft.Extensions.DependencyInjection; @@ -88,8 +91,12 @@ private void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); + + services.AddSingleton(); } protected override async void OnStartup(StartupEventArgs e) @@ -126,11 +133,17 @@ private void SetupGlobalExceptionHandling() protected override async void OnExit(ExitEventArgs e) { + var notificationService = _host.Services.GetService(); + notificationService?.Dispose(); + using (_host) { await _host.StopAsync(TimeSpan.FromSeconds(5)); + _host.Dispose(); } + base.OnExit(e); } + } } \ No newline at end of file diff --git a/FinTrack/Dtos/BudgetDtos/BudgetCreateDto.cs b/FinTrack/Dtos/BudgetDtos/BudgetCreateDto.cs index 8097b9a..1cb71df 100644 --- a/FinTrack/Dtos/BudgetDtos/BudgetCreateDto.cs +++ b/FinTrack/Dtos/BudgetDtos/BudgetCreateDto.cs @@ -8,6 +8,7 @@ public class BudgetCreateDto public string? Description { get; set; } public string Category { get; set; } = null!; public decimal AllocatedAmount { get; set; } + public decimal? ReachedAmount { get; set; } public BaseCurrencyType Currency { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } diff --git a/FinTrack/Dtos/BudgetDtos/BudgetDto.cs b/FinTrack/Dtos/BudgetDtos/BudgetDto.cs index 384cef0..9d9b868 100644 --- a/FinTrack/Dtos/BudgetDtos/BudgetDto.cs +++ b/FinTrack/Dtos/BudgetDtos/BudgetDto.cs @@ -9,6 +9,7 @@ public class BudgetDto public string? Description { get; set; } public string Category { get; set; } = null!; public decimal AllocatedAmount { get; set; } + public decimal? ReachedAmount { get; set; } public BaseCurrencyType Currency { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } diff --git a/FinTrack/Dtos/BudgetDtos/BudgetUpdateDto.cs b/FinTrack/Dtos/BudgetDtos/BudgetUpdateDto.cs index c4ec848..77f272f 100644 --- a/FinTrack/Dtos/BudgetDtos/BudgetUpdateDto.cs +++ b/FinTrack/Dtos/BudgetDtos/BudgetUpdateDto.cs @@ -8,6 +8,7 @@ public class BudgetUpdateDto public string? Description { get; set; } public string Category { get; set; } = null!; public decimal AllocatedAmount { get; set; } + public decimal? ReachedAmount { get; set; } public BaseCurrencyType Currency { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } diff --git a/FinTrack/Dtos/BudgetDtos/BudgetUpdateReachedAmountDto.cs b/FinTrack/Dtos/BudgetDtos/BudgetUpdateReachedAmountDto.cs new file mode 100644 index 0000000..4308e3d --- /dev/null +++ b/FinTrack/Dtos/BudgetDtos/BudgetUpdateReachedAmountDto.cs @@ -0,0 +1,8 @@ +namespace FinTrackForWindows.Dtos.BudgetDtos +{ + public class BudgetUpdateReachedAmountDto + { + public int BudgetId { get; set; } + public decimal? ReachedAmount { get; set; } + } +} diff --git a/FinTrack/Dtos/UserProfileDto.cs b/FinTrack/Dtos/UserProfileDto.cs index 90bcd69..7a4c6fc 100644 --- a/FinTrack/Dtos/UserProfileDto.cs +++ b/FinTrack/Dtos/UserProfileDto.cs @@ -5,8 +5,7 @@ public class UserProfileDto public int Id { get; set; } public string UserName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; - public string ProfilePicture { get; set; } = string.Empty; - public string Role { get; set; } = string.Empty; + public string? ProfilePicture { get; set; } public string MembershipType { get; set; } = string.Empty; } } diff --git a/FinTrack/Enums/ExportFormat.cs b/FinTrack/Enums/DocumentFormat.cs similarity index 100% rename from FinTrack/Enums/ExportFormat.cs rename to FinTrack/Enums/DocumentFormat.cs diff --git a/FinTrack/FinTrackForWindows.csproj b/FinTrack/FinTrackForWindows.csproj index d5e9f6f..c42aee8 100644 --- a/FinTrack/FinTrackForWindows.csproj +++ b/FinTrack/FinTrackForWindows.csproj @@ -8,6 +8,17 @@ true + + + + + + + + + + + @@ -57,6 +68,7 @@ + diff --git a/FinTrack/Helpers/AutoScrollBehavior.cs b/FinTrack/Helpers/AutoScrollBehavior.cs new file mode 100644 index 0000000..b0bae87 --- /dev/null +++ b/FinTrack/Helpers/AutoScrollBehavior.cs @@ -0,0 +1,82 @@ +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; + +namespace FinTrackForWindows.Helpers +{ + public static class AutoScrollBehavior + { + public static readonly DependencyProperty AutoScrollProperty = + DependencyProperty.RegisterAttached( + "AutoScroll", + typeof(bool), + typeof(AutoScrollBehavior), + new PropertyMetadata(false, OnAutoScrollPropertyChanged)); + + public static void SetAutoScroll(DependencyObject obj, bool value) + { + obj.SetValue(AutoScrollProperty, value); + } + + public static bool GetAutoScroll(DependencyObject obj) + { + return (bool)obj.GetValue(AutoScrollProperty); + } + + private static void OnAutoScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not ScrollViewer scrollViewer) + { + return; + } + + if ((bool)e.NewValue) + { + scrollViewer.ScrollToBottom(); + + if (FindItemsControl(scrollViewer) is { } itemsControl && + itemsControl.ItemsSource is INotifyCollectionChanged collection) + { + collection.CollectionChanged += (s, args) => + { + if (args.Action == NotifyCollectionChangedAction.Add) + { + scrollViewer.ScrollToBottom(); + } + }; + } + } + } + + private static ItemsControl? FindItemsControl(DependencyObject parent) + { + if (parent == null) return null; + + if (parent is ItemsControl itemsControl) + { + return itemsControl; + } + + if (parent is ContentPresenter contentPresenter) + { + if (contentPresenter.Content is ItemsControl ic) + { + return ic; + } + } + + if (parent is Panel panel) + { + foreach (var child in panel.Children) + { + if (child is ItemsControl ic) + { + return ic; + } + } + } + + return null; + } + } +} diff --git a/FinTrack/Helpers/BooleanToVisibilityConverter.cs b/FinTrack/Helpers/BooleanToVisibilityConverter.cs index 8904d84..7ce9cff 100644 --- a/FinTrack/Helpers/BooleanToVisibilityConverter.cs +++ b/FinTrack/Helpers/BooleanToVisibilityConverter.cs @@ -4,16 +4,38 @@ namespace FinTrackForWindows.Helpers { + [ValueConversion(typeof(bool), typeof(Visibility))] public class BooleanToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (value is bool b && b) ? Visibility.Collapsed : Visibility.Visible; + bool boolValue = false; + if (value is bool b) + { + boolValue = b; + } + + if (parameter != null && (parameter.ToString().Equals("invert", StringComparison.OrdinalIgnoreCase) || + parameter.ToString().Equals("true", StringComparison.OrdinalIgnoreCase))) + { + boolValue = !boolValue; + } + + return boolValue ? Visibility.Visible : Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + if (value is Visibility visibility && visibility == Visibility.Visible) + { + if (parameter != null && (parameter.ToString().Equals("invert", StringComparison.OrdinalIgnoreCase) || + parameter.ToString().Equals("true", StringComparison.OrdinalIgnoreCase))) + { + return false; + } + return true; + } + return false; } } } \ No newline at end of file diff --git a/FinTrack/Helpers/EqualityToBooleanConverter.cs b/FinTrack/Helpers/EqualityToBooleanConverter.cs index a425724..03c1e2d 100644 --- a/FinTrack/Helpers/EqualityToBooleanConverter.cs +++ b/FinTrack/Helpers/EqualityToBooleanConverter.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Windows; using System.Windows.Data; namespace FinTrackForWindows.Helpers @@ -7,10 +8,19 @@ 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) + if (values.Any(v => v == null || v == DependencyProperty.UnsetValue)) + { return false; + } - return values[0].Equals(values[1]); + for (int i = 1; i < values.Length; i++) + { + if (!object.Equals(values[0], values[i])) + { + return false; + } + } + return true; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) diff --git a/FinTrack/Helpers/ValueToVisibilityConverter.cs b/FinTrack/Helpers/ValueToVisibilityConverter.cs new file mode 100644 index 0000000..6081780 --- /dev/null +++ b/FinTrack/Helpers/ValueToVisibilityConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class ValueToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double number && number > 0) + { + return Visibility.Visible; + } + return 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/Helpers/VideoStatusConverter.cs b/FinTrack/Helpers/VideoStatusConverter.cs new file mode 100644 index 0000000..d490824 --- /dev/null +++ b/FinTrack/Helpers/VideoStatusConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class VideoStatusConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int?) + { + return (int?)value != null ? "Uploaded" : "Not Uploaded"; + } + return "Unknown"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/FinTrack/Models/Budget/BudgetModel.cs b/FinTrack/Models/Budget/BudgetModel.cs index 555542d..e829112 100644 --- a/FinTrack/Models/Budget/BudgetModel.cs +++ b/FinTrack/Models/Budget/BudgetModel.cs @@ -29,7 +29,17 @@ public partial class BudgetModel : ObservableObject private decimal currentAmount; [ObservableProperty] - private BaseCurrencyType currency = BaseCurrencyType.USD; + [NotifyPropertyChangedFor(nameof(ProgressValue))] + [NotifyPropertyChangedFor(nameof(ProgressText))] + private decimal? reachedAmount; + + [ObservableProperty] + private bool isEditingReachedAmount = false; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ProgressText))] + [NotifyPropertyChangedFor(nameof(ReachedAmountDisplayText))] + private BaseCurrencyType currency; [ObservableProperty] [NotifyPropertyChangedFor(nameof(RemainingTimeText))] @@ -39,14 +49,31 @@ public partial class BudgetModel : ObservableObject [NotifyPropertyChangedFor(nameof(RemainingTimeText))] private DateTime endDate = DateTime.Today.AddMonths(1); - public double ProgressValue => AllocatedAmount <= 0 ? 0 : Math.Max(0, Math.Min(100, (double)(CurrentAmount / AllocatedAmount) * 100)); + public double ProgressValue => AllocatedAmount <= 0 ? 0 : Math.Max(0, Math.Min(100, (double)((ReachedAmount ?? 0) / AllocatedAmount) * 100)); public string ProgressText { get { - var culture = new CultureInfo("tr-TR"); - return $"{CurrentAmount.ToString("C", culture)} / {AllocatedAmount.ToString("C", culture)}"; + var current = ReachedAmount ?? 0; + string currencyCode = Currency.ToString(); + var culture = CultureInfo.CurrentCulture; + + string currentText = current.ToString("N2", culture); + string allocatedText = AllocatedAmount.ToString("N2", culture); + + return $"{currentText} {currencyCode} / {allocatedText} {currencyCode}"; + } + } + + public string ReachedAmountDisplayText + { + get + { + var current = ReachedAmount ?? 0; + string currencyCode = Currency.ToString(); + var culture = CultureInfo.CurrentCulture; + return $"{current.ToString("N2", culture)} {currencyCode}"; } } diff --git a/FinTrack/Models/Dashboard/AccountDashboard.cs b/FinTrack/Models/Dashboard/AccountDashboard.cs index 8358139..4ab98fa 100644 --- a/FinTrack/Models/Dashboard/AccountDashboard.cs +++ b/FinTrack/Models/Dashboard/AccountDashboard.cs @@ -1,12 +1,14 @@ -using System.Windows.Media; - -namespace FinTrackForWindows.Models.Dashboard +namespace FinTrackForWindows.Models.Dashboard { public class AccountDashboard { public string Name { get; set; } = string.Empty; - public double Percentage { get; set; } public string Balance { get; set; } = string.Empty; - public Brush ProgressBarBrush { get; set; } = Brushes.Transparent; + + public double IncomePercentage { get; set; } + public double ExpensePercentage { get; set; } + + public string IncomeAmountText { get; set; } = string.Empty; + public string ExpenseAmountText { get; set; } = string.Empty; } -} +} \ No newline at end of file diff --git a/FinTrack/Models/Dashboard/ReportDashboardModel.cs b/FinTrack/Models/Dashboard/ReportDashboardModel.cs index 80f7857..28e4fb3 100644 --- a/FinTrack/Models/Dashboard/ReportDashboardModel.cs +++ b/FinTrack/Models/Dashboard/ReportDashboardModel.cs @@ -1,6 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.ReportDtos; using FinTrackForWindows.Enums; +using FinTrackForWindows.Helpers; +using FinTrackForWindows.Services.Reports; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Windows; @@ -12,13 +15,18 @@ public partial class ReportDashboardModel : ObservableObject [ObservableProperty] private string name = string.Empty; + public ReportType Type { get; set; } + public ObservableCollection Formats { get; set; } private readonly ILogger _logger; - public ReportDashboardModel(ILogger logger) + private readonly IReportStore _reportStore; + + public ReportDashboardModel(ILogger logger, IReportStore reportStore) { _logger = logger; + _reportStore = reportStore; Formats = new ObservableCollection(); @@ -29,11 +37,37 @@ public ReportDashboardModel(ILogger logger) } [RelayCommand] - private void Generate(DocumentFormat format) + private async Task Generate(DocumentFormat format) { + _logger.LogInformation("Hızlı rapor oluşturuluyor -> Rapor Adı: {ReportName}, Format: {Format}", this.Name, format); + + var reportRequest = new ReportRequestDto + { + ReportType = Type, + ExportFormat = format, + StartDate = new DateTime(2025, 1, 1), + EndDate = new DateTime(2025, 12, 30), + MinBalance = null, + MaxBalance = null, + IsIncomeSelected = false, + IsExpenseSelected = false, + SelectedSortingCriterion = null, + SelectedAccountIds = null, + SelectedCategoryIds = null, + }; + + string? savedPath = await _reportStore.CreateAndSaveReportAsync(reportRequest); - _logger.LogInformation("Rapor oluşturuluyor -> Rapor Adı: {ReportName}, Format: {Format}", this.Name, format); - MessageBox.Show($"Rapor oluşturuldu:\nAd: {this.Name}\nFormat: {format}", "Rapor Oluşturma", MessageBoxButton.OK, MessageBoxImage.Information); + if (!string.IsNullOrEmpty(savedPath)) + { + MessageBox.Show($"Report created successfully and saved to '{savedPath}'.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + FileSaver.OpenContainingFolder(savedPath); + } + else + { + _logger.LogWarning("ReportStore reported failure in report creation (no data or server error)."); + MessageBox.Show("Could not create report. No data found for the specified criteria or a server error occurred.", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning); + } } } } diff --git a/FinTrack/Models/Debt/DebtModel.cs b/FinTrack/Models/Debt/DebtModel.cs index edcb595..1f5897e 100644 --- a/FinTrack/Models/Debt/DebtModel.cs +++ b/FinTrack/Models/Debt/DebtModel.cs @@ -10,7 +10,6 @@ public partial class DebtModel : ObservableObject public int LenderId { get; set; } public int BorrowerId { get; set; } public int CurrentUserId { get; set; } - public int? VideoMetadataId { get; set; } [ObservableProperty] @@ -23,12 +22,38 @@ public partial class DebtModel : ObservableObject private decimal amount; [ObservableProperty] + private BaseCurrencyType currency; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DaysUntilDue))] [NotifyPropertyChangedFor(nameof(CanMarkAsDefaulted))] private DateTime dueDate; - public string borrowerImageUrl = string.Empty; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CounterPartyImageUrl))] + private string lenderImageUrl = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CounterPartyImageUrl))] + private string borrowerImageUrl = string.Empty; - public string lenderImageUrl = string.Empty; + [ObservableProperty] + private string lenderEmail = string.Empty; + + [ObservableProperty] + private string borrowerEmail = string.Empty; + + [ObservableProperty] + private string? description; + + [ObservableProperty] + private DateTime? acceptanceDate; + + [ObservableProperty] + private DateTime? operatorApprovalDate; + + [ObservableProperty] + private DateTime? paymentDate; [ObservableProperty] [NotifyPropertyChangedFor(nameof(StatusText))] @@ -39,16 +64,16 @@ public partial class DebtModel : ObservableObject [NotifyPropertyChangedFor(nameof(CanMarkAsDefaulted))] private DebtStatusType status; + [ObservableProperty] + private bool isExpanded; + public bool IsCurrentUserTheBorrower => BorrowerId == CurrentUserId; public bool IsCurrentUserTheLender => LenderId == CurrentUserId; - public string DebtTitle => IsCurrentUserTheBorrower - ? $"Alacaklı: {LenderName}" - : $"Borçlu: {BorrowerName}"; + public int DaysUntilDue => (DueDate.Date - DateTime.Now.Date).Days; - public string UserIconPath => IsCurrentUserTheBorrower - ? "/Assets/Images/Icons/user-red.png" - : "/Assets/Images/Icons/user-green.png"; + public string CounterPartyName => IsCurrentUserTheBorrower ? LenderName : BorrowerName; + public string CounterPartyImageUrl => IsCurrentUserTheBorrower ? LenderImageUrl : BorrowerImageUrl; public Brush StatusBrush => Status switch { @@ -82,20 +107,14 @@ public partial class DebtModel : ObservableObject public bool IsRejected => Status == DebtStatusType.RejectedByBorrower || Status == DebtStatusType.RejectedByOperator; - /// - /// "Teminat Videosunu İzle" düğmesinin görünür olup olmayacağını belirler. - /// public bool IsVideoViewableForLender => - Status == DebtStatusType.Defaulted && // Durum 'Defaulted' olmalı - IsCurrentUserTheLender && // Kullanıcı borç veren olmalı - VideoMetadataId.HasValue; // İlişkili bir video olmalı + Status == DebtStatusType.Defaulted && + IsCurrentUserTheLender && + VideoMetadataId.HasValue; - /// - /// "Temerrüde Düştü Olarak İşaretle" düğmesinin görünür olup olmayacağını belirleyecek. - /// public bool CanMarkAsDefaulted => - Status == DebtStatusType.Active && // Durum 'Active' olmalı - IsCurrentUserTheLender && // Kullanıcı borç veren olmalı - DueDate < DateTime.Now; // Vadesi geçmiş olmalı + Status == DebtStatusType.Active && + IsCurrentUserTheLender && + DueDate.Date < DateTime.Now.Date; } } \ No newline at end of file diff --git a/FinTrack/Models/Report/SelectableOptionReport.cs b/FinTrack/Models/Report/SelectableOptionReport.cs index 9037435..4a4ca12 100644 --- a/FinTrack/Models/Report/SelectableOptionReport.cs +++ b/FinTrack/Models/Report/SelectableOptionReport.cs @@ -12,11 +12,11 @@ public partial class SelectableOptionReport : ObservableObject [ObservableProperty] private bool isSelected; - public SelectableOptionReport(int _id, string _name, bool _isSelected = false) + public SelectableOptionReport(int id, string name, bool isSelected = false) { - name = _name; - isSelected = _isSelected; - Id = _id; + this.Id = id; + this.name = name; + this.isSelected = isSelected; } } -} +} \ No newline at end of file diff --git a/FinTrack/Models/User/UserModel.cs b/FinTrack/Models/User/UserModel.cs new file mode 100644 index 0000000..89ccefe --- /dev/null +++ b/FinTrack/Models/User/UserModel.cs @@ -0,0 +1,12 @@ +namespace FinTrackForWindows.Models.User +{ + public class UserModel + { + public int Id { get; set; } + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string ProfilePictureUrl { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public string MembershipPlan { get; set; } = string.Empty; + } +} diff --git a/FinTrack/Services/Accounts/AccountStore.cs b/FinTrack/Services/Accounts/AccountStore.cs index dec819e..b347202 100644 --- a/FinTrack/Services/Accounts/AccountStore.cs +++ b/FinTrack/Services/Accounts/AccountStore.cs @@ -33,7 +33,7 @@ public async Task LoadAccountsAsync() { if (_accounts.Any()) { - _logger.LogInformation("Hesaplar zaten yüklü. API çağrısı atlanıyor."); + _logger.LogInformation("Accounts are already loaded. Skipping API call."); return; } @@ -54,11 +54,11 @@ public async Task LoadAccountsAsync() Balance = dto.Balance, }); } - _logger.LogInformation("{Count} adet hesap Accountstore'a yüklendi.", _accounts.Count); + _logger.LogInformation("{Count} accounts loaded into AccountStore.", _accounts.Count); } catch (Exception ex) { - _logger.LogError(ex, "Accountstore'da hesaplar yüklenirken hata oluştu."); + _logger.LogError(ex, "An error occurred while loading accounts in AccountStore."); } } diff --git a/FinTrack/Services/Api/ApiService.cs b/FinTrack/Services/Api/ApiService.cs index 50a70c1..f6452e8 100644 --- a/FinTrack/Services/Api/ApiService.cs +++ b/FinTrack/Services/Api/ApiService.cs @@ -19,7 +19,6 @@ public class ApiService : IApiService private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly IConfiguration _configuration; - public ApiService(ILogger logger, IConfiguration configuration) { _logger = logger; @@ -52,10 +51,10 @@ private void AddAuthorizationHeader() public async Task DeleteAsync(string endpoint) { - _logger.LogInformation("DELETE isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating DELETE request: {Endpoint}", endpoint); if (string.IsNullOrWhiteSpace(endpoint)) { - _logger.LogError("DELETE isteği sırasında endpoint boş veya null: {Endpoint}", endpoint); + _logger.LogError("DELETE request failed: endpoint is null or empty. Endpoint: {Endpoint}", endpoint); throw new ArgumentException("Endpoint cannot be null or empty", nameof(endpoint)); } @@ -69,24 +68,24 @@ private void AddAuthorizationHeader() var stream = await response.Content.ReadAsStreamAsync(); var result = await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions); - _logger.LogInformation("DELETE isteği başarılı: {Endpoint}", endpoint); + _logger.LogInformation("DELETE request succeeded: {Endpoint}", endpoint); return result; } catch (HttpRequestException ex) { - _logger.LogError(ex, "DELETE isteği sırasında HTTP hatası oluştu: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); + _logger.LogError(ex, "HTTP error during DELETE request. Endpoint: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); return default(T); } catch (Exception ex) { - _logger.LogError(ex, "DELETE isteği sırasında genel bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during DELETE request. Endpoint: {Endpoint}", endpoint); return default(T); } } public async Task GetAsync(string endpoint) { - _logger.LogInformation("GET isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating GET request: {Endpoint}", endpoint); try { AddAuthorizationHeader(); @@ -97,29 +96,29 @@ private void AddAuthorizationHeader() var strean = await response.Content.ReadAsStreamAsync(); var result = await _httpClient.GetFromJsonAsync(endpoint, _jsonSerializerOptions); - _logger.LogInformation("GET isteği başarılı: {Endpoint}", endpoint); + _logger.LogInformation("GET request succeeded: {Endpoint}", endpoint); return result; } catch (HttpRequestException ex) { - _logger.LogError(ex, "GET isteği sırasında HTTP hatası oluştu: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); + _logger.LogError(ex, "HTTP error during GET request. Endpoint: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); return default(T); } catch (JsonException ex) { - _logger.LogError(ex, "GET isteği sırasında JSON serileştirme hatası oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "JSON deserialization error during GET request. Endpoint: {Endpoint}", endpoint); return default(T); } catch (Exception ex) { - _logger.LogError(ex, "GET isteği sırasında beklenmeyen bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during GET request. Endpoint: {Endpoint}", endpoint); return default(T); } } public async Task?> GetsAsync(string endpoint) { - _logger.LogInformation("GET (list) isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating GET (list) request: {Endpoint}", endpoint); try { AddAuthorizationHeader(); @@ -129,37 +128,37 @@ private void AddAuthorizationHeader() var result = await _httpClient.GetFromJsonAsync>(endpoint, _jsonSerializerOptions); - _logger.LogInformation("GET (list) isteği başarılı: {Endpoint}", endpoint); + _logger.LogInformation("GET (list) request succeeded: {Endpoint}", endpoint); return result; } catch (HttpRequestException ex) { - _logger.LogError(ex, "GET (list) isteği sırasında HTTP hatası oluştu: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); + _logger.LogError(ex, "HTTP error during GET (list) request. Endpoint: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); return default(List); } catch (JsonException ex) { - _logger.LogError(ex, "GET (list) isteği sırasında JSON serileştirme hatası oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "JSON deserialization error during GET (list) request. Endpoint: {Endpoint}", endpoint); return default(List); } catch (Exception ex) { - _logger.LogError(ex, "GET (list) isteği sırasında beklenmeyen bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during GET (list) request. Endpoint: {Endpoint}", endpoint); return default(List); } } public async Task PostAsync(string endpoint, object data) { - _logger.LogInformation("POST isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating POST request: {Endpoint}", endpoint); if (string.IsNullOrWhiteSpace(endpoint)) { - _logger.LogError("POST isteği sırasında endpoint boş veya null: {Endpoint}", endpoint); + _logger.LogError("POST request failed: endpoint is null or empty. Endpoint: {Endpoint}", endpoint); throw new ArgumentException("Endpoint cannot be null or empty", nameof(endpoint)); } if (data == null) { - _logger.LogError("POST isteği sırasında veri null: {Endpoint}", endpoint); + _logger.LogError("POST request failed: data is null. Endpoint: {Endpoint}", endpoint); throw new ArgumentNullException(nameof(data), "Data cannot be null"); } @@ -176,32 +175,32 @@ private void AddAuthorizationHeader() var stream = await response.Content.ReadAsStreamAsync(); var result = await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions); - _logger.LogInformation("POST isteği başarılı: {Endpoint}", endpoint); + _logger.LogInformation("POST request succeeded: {Endpoint}", endpoint); return result; } catch (HttpRequestException ex) { - _logger.LogError(ex, "POST isteği sırasında HTTP hatası oluştu: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); + _logger.LogError(ex, "HTTP error during POST request. Endpoint: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); return default(T); } catch (Exception ex) { - _logger.LogError(ex, "POST isteği sırasında genel bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during POST request. Endpoint: {Endpoint}", endpoint); return default(T); } } public async Task PutAsync(string endpoint, object data) { - _logger.LogInformation("PUT isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating PUT request: {Endpoint}", endpoint); if (string.IsNullOrWhiteSpace(endpoint)) { - _logger.LogError("PUT isteği sırasında endpoint boş veya null: {Endpoint}", endpoint); + _logger.LogError("PUT request failed: endpoint is null or empty. Endpoint: {Endpoint}", endpoint); throw new ArgumentException("Endpoint cannot be null or empty", nameof(endpoint)); } if (data == null) { - _logger.LogError("PUT isteği sırasında veri null: {Endpoint}", endpoint); + _logger.LogError("PUT request failed: data is null. Endpoint: {Endpoint}", endpoint); throw new ArgumentNullException(nameof(data), "Data cannot be null"); } @@ -218,32 +217,32 @@ private void AddAuthorizationHeader() var stream = await response.Content.ReadAsStreamAsync(); var result = await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions); - _logger.LogInformation("PUT isteği başarılı: {Endpoint}", endpoint); + _logger.LogInformation("PUT request succeeded: {Endpoint}", endpoint); return result; } catch (HttpRequestException ex) { - _logger.LogError(ex, "PUT isteği sırasında HTTP hatası oluştu: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); + _logger.LogError(ex, "HTTP error during PUT request. Endpoint: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); return default(T); } catch (Exception ex) { - _logger.LogError(ex, "PUT isteği sırasında genel bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during PUT request. Endpoint: {Endpoint}", endpoint); return default(T); } } public async Task CreateCategoryAsync(string endpoint, object payload) { - _logger.LogInformation("Kategori oluşturma (POST) isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating category creation (POST) request: {Endpoint}", endpoint); if (string.IsNullOrWhiteSpace(endpoint)) { - _logger.LogError("Kategori oluşturma isteği sırasında endpoint boş veya null."); + _logger.LogError("Category creation request failed: endpoint is null or empty."); 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."); + _logger.LogError("Category creation request failed: payload is null."); throw new ArgumentNullException(nameof(payload)); } @@ -257,32 +256,32 @@ public async Task CreateCategoryAsync(string endpoint, object payload) var result = await response.Content.ReadFromJsonAsync(_jsonSerializerOptions); - _logger.LogInformation("Kategori oluşturma (POST) isteği başarılı: {Endpoint}", endpoint); + _logger.LogInformation("Category creation (POST) request succeeded: {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); + _logger.LogError(ex, "HTTP error during category creation. Endpoint: {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); + _logger.LogError(ex, "JSON deserialization error during category creation. Ensure the API returns 'true' or 'false'. Endpoint: {Endpoint}", endpoint); return false; } catch (Exception ex) { - _logger.LogError(ex, "Kategori oluşturma sırasında genel bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during category creation. Endpoint: {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); + _logger.LogInformation("Initiating file upload request: {Endpoint}, File: {FilePath}", endpoint, filePath); if (!File.Exists(filePath)) { - _logger.LogError("Yüklenecek dosya bulunamadı: {FilePath}", filePath); + _logger.LogError("File upload failed: file not found. File: {FilePath}", filePath); return false; } @@ -302,35 +301,35 @@ public async Task UploadFileAsync(string endpoint, string filePath) if (response.IsSuccessStatusCode) { - _logger.LogInformation("Dosya başarıyla yüklendi: {Endpoint}", endpoint); + _logger.LogInformation("File uploaded successfully: {Endpoint}", endpoint); return true; } else { var errorContent = await response.Content.ReadAsStringAsync(); - _logger.LogError("Dosya yükleme sırasında HTTP hatası: {StatusCode} - {Error}", response.StatusCode, errorContent); + _logger.LogError("HTTP error during file upload. Status Code: {StatusCode}. Error: {Error}", response.StatusCode, errorContent); return false; } } } catch (Exception ex) { - _logger.LogError(ex, "Dosya yükleme sırasında genel bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during file upload. Endpoint: {Endpoint}", endpoint); return false; } } public async Task<(byte[] FileBytes, string FileName)?> PostAndDownloadReportAsync(string endpoint, T payload) { - _logger.LogInformation("Rapor indirme (POST) isteği başlatılıyor: {Endpoint}", endpoint); + _logger.LogInformation("Initiating report download (POST) request: {Endpoint}", endpoint); if (string.IsNullOrWhiteSpace(endpoint)) { - _logger.LogError("Rapor indirme isteği sırasında endpoint boş veya null."); + _logger.LogError("Report download request failed: endpoint is null or empty."); throw new ArgumentException("Endpoint cannot be null or empty", nameof(endpoint)); } if (payload == null) { - _logger.LogError("Rapor indirme isteği sırasında gönderilecek veri (payload) null."); + _logger.LogError("Report download request failed: payload is null."); throw new ArgumentNullException(nameof(payload)); } @@ -354,37 +353,37 @@ public async Task UploadFileAsync(string endpoint, string filePath) format = dto.ExportFormat.ToString().ToLower(); } fileName = $"report_{DateTime.Now:yyyyMMddHHmmss}.{format}"; - _logger.LogWarning("Content-Disposition başlığı bulunamadı. Varsayılan dosya adı kullanılıyor: {FileName}", fileName); + _logger.LogWarning("Content-Disposition header not found. Using default file name: {FileName}", fileName); } byte[] fileBytes = await response.Content.ReadAsByteArrayAsync(); - _logger.LogInformation("Rapor başarıyla indirildi: {FileName}, Boyut: {Size} bytes", fileName, fileBytes.Length); + _logger.LogInformation("Report downloaded successfully: {FileName}, Size: {Size} bytes", fileName, fileBytes.Length); return (fileBytes, fileName); } else { var errorContent = await response.Content.ReadAsStringAsync(); - _logger.LogError("Rapor indirme sırasında HTTP hatası: {StatusCode} - {Error}", response.StatusCode, errorContent); + _logger.LogError("HTTP error during report download. Status Code: {StatusCode}. Error: {Error}", response.StatusCode, errorContent); - throw new HttpRequestException($"API'den rapor alınamadı. Sunucu '{response.StatusCode}' durum kodu ile cevap verdi. Detay: {errorContent}"); + throw new HttpRequestException($"Failed to download report from API. Server responded with status code '{response.StatusCode}'. Details: {errorContent}"); } } catch (HttpRequestException ex) { - _logger.LogError(ex, "Rapor indirme sırasında HTTP isteği hatası: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error during report download. Endpoint: {Endpoint}", endpoint); throw; } catch (Exception ex) { - _logger.LogError(ex, "Rapor indirme sırasında genel bir hata oluştu: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during report download. Endpoint: {Endpoint}", endpoint); throw; } } public async Task<(Stream? Stream, string? ContentType, string? FileName)> StreamFileAsync(string endpoint) { - _logger.LogInformation("Streaming file request started: {Endpoint}", endpoint); + _logger.LogInformation("Initiating file streaming request: {Endpoint}", endpoint); try { AddAuthorizationHeader(); @@ -398,17 +397,17 @@ public async Task UploadFileAsync(string endpoint, string filePath) var contentType = response.Content.Headers.ContentType?.ToString(); var fileName = response.Content.Headers.ContentDisposition?.FileName?.Trim('"'); - _logger.LogInformation("File stream successfully retrieved from {Endpoint}", endpoint); + _logger.LogInformation("File stream retrieved successfully from {Endpoint}", endpoint); return (stream, contentType, fileName); } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error during file stream: {Endpoint}. Status: {StatusCode}", endpoint, ex.StatusCode); + _logger.LogError(ex, "HTTP error during file streaming. Endpoint: {Endpoint}. Status Code: {StatusCode}", endpoint, ex.StatusCode); return (null, null, null); } catch (Exception ex) { - _logger.LogError(ex, "Generic error during file stream: {Endpoint}", endpoint); + _logger.LogError(ex, "Unexpected error during file streaming. Endpoint: {Endpoint}", endpoint); return (null, null, null); } } diff --git a/FinTrack/Services/AppInNotifications/AppInNotificationService.cs b/FinTrack/Services/AppInNotifications/AppInNotificationService.cs new file mode 100644 index 0000000..d311f60 --- /dev/null +++ b/FinTrack/Services/AppInNotifications/AppInNotificationService.cs @@ -0,0 +1,111 @@ +using Notifications.Wpf; +using System.Windows; + +namespace FinTrackForWindows.Services.AppInNotifications +{ + public class AppInNotificationService : IAppInNotificationService + { + private readonly NotificationManager _notificationManager; + + public AppInNotificationService() + { + _notificationManager = new NotificationManager(); + } + + public void ShowError(string message) + { + _notificationManager.Show(new NotificationContent + { + Title = "Error", + Message = message, + Type = NotificationType.Error + }); + } + + public void ShowInfo(string message) + { + _notificationManager.Show(new NotificationContent + { + Title = "Information", + Message = message, + Type = NotificationType.Information + }); + } + + public void ShowSuccess(string message) + { + _notificationManager.Show(new NotificationContent + { + Title = "Success", + Message = message, + Type = NotificationType.Success + }); + } + + public void ShowWarning(string message) + { + _notificationManager.Show(new NotificationContent + { + Title = "Warning", + Message = message, + Type = NotificationType.Warning + }); + } + + + public void ShowError(string message, Exception exception) + { + _notificationManager.Show(new NotificationContent + { + Title = "Error", + Message = $"{message}\n{exception.Message}", + Type = NotificationType.Error + }); + } + + public void ShowInfo(string message, Exception exception) + { + _notificationManager.Show(new NotificationContent + { + Title = "Information", + Message = $"{message}\n{exception.Message}", + Type = NotificationType.Information + }); + } + + public void ShowSuccess(string message, Exception exception) + { + _notificationManager.Show(new NotificationContent + { + Title = "Success", + Message = $"{message}\n{exception.Message}", + Type = NotificationType.Success + }); + } + + public void ShowWarning(string message, Exception exception) + { + _notificationManager.Show(new NotificationContent + { + Title = "Warning", + Message = $"{message}\n{exception.Message}", + Type = NotificationType.Warning + }); + } + + public void Dispose() + { + foreach (Window window in Application.Current.Windows) + { + if (window.GetType().Name == "NotificationWindow") + { + try + { + window.Close(); + } + catch { /* Pencere zaten kapanmış */ } + } + } + } + } +} diff --git a/FinTrack/Services/AppInNotifications/IAppInNotificationService.cs b/FinTrack/Services/AppInNotifications/IAppInNotificationService.cs new file mode 100644 index 0000000..2ffec7d --- /dev/null +++ b/FinTrack/Services/AppInNotifications/IAppInNotificationService.cs @@ -0,0 +1,15 @@ +namespace FinTrackForWindows.Services.AppInNotifications +{ + public interface IAppInNotificationService : IDisposable + { + void ShowSuccess(string message); + void ShowInfo(string message); + void ShowWarning(string message); + void ShowError(string message); + + void ShowError(string message, Exception exception); + void ShowInfo(string message, Exception exception); + void ShowSuccess(string message, Exception exception); + void ShowWarning(string message, Exception exception); + } +} diff --git a/FinTrack/Services/Budgets/BudgetStore.cs b/FinTrack/Services/Budgets/BudgetStore.cs index 78ef27a..9c3d9d3 100644 --- a/FinTrack/Services/Budgets/BudgetStore.cs +++ b/FinTrack/Services/Budgets/BudgetStore.cs @@ -33,7 +33,7 @@ public async Task LoadBudgetsAsync() { if (_budgets.Any()) { - _logger.LogInformation("Bütçeler zaten yüklü. API çağrısı atlanıyor."); + _logger.LogInformation("Budgets are already loaded. Skipping API call."); return; } @@ -52,18 +52,18 @@ public async Task LoadBudgetsAsync() Description = dto.Description, Category = dto.Category, AllocatedAmount = dto.AllocatedAmount, + ReachedAmount = dto.ReachedAmount, Currency = dto.Currency, StartDate = dto.StartDate, EndDate = dto.EndDate, }); } - _logger.LogInformation("{Count} adet bütçe BudgetStore'a yüklendi.", _budgets.Count); + _logger.LogInformation("{Count} budgets loaded into BudgetStore.", _budgets.Count); } catch (Exception ex) { - _logger.LogError(ex, "BudgetStore'da bütçeler yüklenirken hata oluştu."); + _logger.LogError(ex, "An error occurred while loading budgets in BudgetStore."); } - } public async Task AddBudgetAsync(BudgetCreateDto newBudgetDto) @@ -78,6 +78,7 @@ public async Task AddBudgetAsync(BudgetCreateDto newBudgetDto) Description = createdBudgetDto.Description, Category = createdBudgetDto.Category, AllocatedAmount = createdBudgetDto.AllocatedAmount, + ReachedAmount = createdBudgetDto.ReachedAmount, Currency = createdBudgetDto.Currency, StartDate = createdBudgetDto.StartDate, EndDate = createdBudgetDto.EndDate @@ -108,6 +109,7 @@ public async Task UpdateBudgetAsync(int budgetId, BudgetCreateDto updatedBudget) item.Description = updatedBudget.Description; item.Category = updatedBudget.Category; item.AllocatedAmount = updatedBudget.AllocatedAmount; + item.ReachedAmount = updatedBudget.ReachedAmount; item.Currency = updatedBudget.Currency; item.StartDate = updatedBudget.StartDate; item.EndDate = updatedBudget.EndDate; @@ -116,6 +118,21 @@ public async Task UpdateBudgetAsync(int budgetId, BudgetCreateDto updatedBudget) } } + public async Task UpdateReachedAmountAsync(BudgetUpdateReachedAmountDto updatedBudget) + { + var updateBudgetDto = await _apiService.PutAsync($"Budgets/Update-Reached-Amount", updatedBudget); + if (updateBudgetDto != null) + { + foreach (var item in _budgets) + { + if (item.Id == updateBudgetDto.Id) + { + item.ReachedAmount = updatedBudget.ReachedAmount; + } + } + } + } + private void OnInternalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { BudgetsChanged?.Invoke(this, e); diff --git a/FinTrack/Services/Budgets/IBudgetStore.cs b/FinTrack/Services/Budgets/IBudgetStore.cs index 2f32696..18f8240 100644 --- a/FinTrack/Services/Budgets/IBudgetStore.cs +++ b/FinTrack/Services/Budgets/IBudgetStore.cs @@ -15,6 +15,7 @@ public interface IBudgetStore Task AddBudgetAsync(BudgetCreateDto newBudget); Task UpdateBudgetAsync(int budgetId, BudgetCreateDto updatedBudget); + Task UpdateReachedAmountAsync(BudgetUpdateReachedAmountDto newBudget); Task DeleteBudgetAsync(int budgetId); } } diff --git a/FinTrack/Services/Currencies/CurrenciesStore.cs b/FinTrack/Services/Currencies/CurrenciesStore.cs index bcdad83..69ad05c 100644 --- a/FinTrack/Services/Currencies/CurrenciesStore.cs +++ b/FinTrack/Services/Currencies/CurrenciesStore.cs @@ -28,7 +28,7 @@ public async Task LoadCurrenciesAsync() { if (_currencies.Any()) { - _logger.LogInformation("Para birimleri zaten yüklü. API çağrısı atlanıyor."); + _logger.LogInformation("Currencies are already loaded. Skipping API call."); return; } @@ -49,12 +49,12 @@ public async Task LoadCurrenciesAsync() ToCurrencyPrice = item.Rate }); } - _logger.LogInformation("{Count} adet para birimi CurrenciesStore'a yüklendi.", _currencies.Count); + _logger.LogInformation("{Count} currencies loaded into CurrenciesStore.", _currencies.Count); } } catch (Exception ex) { - _logger.LogError(ex, "CurrenciesStore'da para birimi listesi yüklenirken hata oluştu."); + _logger.LogError(ex, "An error occurred while loading the currency list in CurrenciesStore."); } } @@ -62,7 +62,7 @@ public async Task LoadCurrenciesAsync() { try { - _logger.LogInformation("{Target} için geçmiş veriler {Period} periyoduyla isteniyor.", targetCurrencyCode, period); + _logger.LogInformation("Historical data for {Target} is requested with {Period} period.", targetCurrencyCode, period); string baseCurrency = "USD"; string endpoint = $"Currency/{baseCurrency}/history/{targetCurrencyCode}?period={period}"; @@ -72,7 +72,7 @@ public async Task LoadCurrenciesAsync() } catch (Exception ex) { - _logger.LogError(ex, "{Target} için geçmiş veriler yüklenirken CurrenciesStore'da hata oluştu.", targetCurrencyCode); + _logger.LogError(ex, "An error occurred while loading historical data for {Target} in CurrenciesStore.", targetCurrencyCode); return null; } } @@ -81,7 +81,7 @@ public async Task GetConvertCurrencies(string fromCurrencyCode, string { try { - _logger.LogInformation("{From} para biriminden {To} para birimine {Amount} miktarı dönüştürülüyor.", fromCurrencyCode, toCurrencyCode, amount); + _logger.LogInformation("Converting {Amount} from {From} to {To}.", amount, fromCurrencyCode, toCurrencyCode); string endpoint = $"Currency/convert?from={fromCurrencyCode}&to={toCurrencyCode}&amount={amount}"; var response = await _apiService.GetAsync(endpoint); if (response != null) @@ -90,13 +90,13 @@ public async Task GetConvertCurrencies(string fromCurrencyCode, string } else { - _logger.LogWarning("Dönüştürme işlemi için geçerli bir yanıt alınamadı."); + _logger.LogWarning("A valid response could not be obtained for the conversion operation."); 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); + _logger.LogError(ex, "An error occurred during the conversion from {From} to {To} in CurrenciesStore.", fromCurrencyCode, toCurrencyCode); return 0; } } diff --git a/FinTrack/Services/Debts/DebtStore.cs b/FinTrack/Services/Debts/DebtStore.cs index 8e8835b..5bdd17b 100644 --- a/FinTrack/Services/Debts/DebtStore.cs +++ b/FinTrack/Services/Debts/DebtStore.cs @@ -74,9 +74,15 @@ public async Task LoadDebtsAsync() 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", + BorrowerImageUrl = dto.BorrowerProfilePicture ?? "/Assets/Images/Icons/user-red.png", + LenderImageUrl = dto.LenderProfilePicture ?? "/Assets/Images/Icons/user-green.png", + BorrowerEmail = dto.BorrowerEmail, + LenderEmail = dto.LenderEmail, + Description = dto.Description, + PaymentDate = dto.PaidAtUtc, + OperatorApprovalDate = dto.OperatorApprovalAtUtc, Amount = dto.Amount, + Currency = dto.Currency, DueDate = dto.DueDateUtc.ToLocalTime(), Status = dto.Status, VideoMetadataId = dto.VideoMetadataId @@ -109,7 +115,7 @@ public async Task LoadDebtsAsync() } } - public async Task SendOfferAsync(string borrowerEmail, decimal amount, string currency, DateTime dueDate, string description) + public async Task SendOfferAsync(string borrowerEmail, decimal amount, BaseCurrencyType currency, DateTime dueDate, string description) { IsLoading = true; try @@ -118,7 +124,7 @@ public async Task SendOfferAsync(string borrowerEmail, decimal amount, string cu { BorrowerEmail = borrowerEmail, Amount = amount, - CurrencyCode = BaseCurrencyType.TRY, + CurrencyCode = currency, DueDateUtc = dueDate.ToUniversalTime(), Description = description }; diff --git a/FinTrack/Services/Debts/IDebtStore.cs b/FinTrack/Services/Debts/IDebtStore.cs index e5df87d..26bd376 100644 --- a/FinTrack/Services/Debts/IDebtStore.cs +++ b/FinTrack/Services/Debts/IDebtStore.cs @@ -1,4 +1,5 @@ -using FinTrackForWindows.Models.Debt; +using FinTrackForWindows.Enums; +using FinTrackForWindows.Models.Debt; using System.Collections.ObjectModel; using System.IO; @@ -13,7 +14,7 @@ public interface IDebtStore bool IsLoading { get; } Task LoadDebtsAsync(); - Task SendOfferAsync(string borrowerEmail, decimal amount, string currency, DateTime dueDate, string description); + Task SendOfferAsync(string borrowerEmail, decimal amount, BaseCurrencyType currency, DateTime dueDate, string description); Task RespondToOfferAsync(DebtModel debt, bool accepted); Task UploadVideoAsync(DebtModel debt, string filePath); diff --git a/FinTrack/Services/Reports/IReportStore.cs b/FinTrack/Services/Reports/IReportStore.cs new file mode 100644 index 0000000..b6f4720 --- /dev/null +++ b/FinTrack/Services/Reports/IReportStore.cs @@ -0,0 +1,22 @@ +using FinTrackForWindows.Dtos.ReportDtos; +using FinTrackForWindows.Enums; +using FinTrackForWindows.Models.Report; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Reports +{ + public interface IReportStore + { + ObservableCollection AvailableReportTypes { get; } + ObservableCollection AvailableAccounts { get; } + ObservableCollection AvailableCategories { get; } + ObservableCollection SortingCriteria { get; } + ObservableCollection AvailableDocumentFormats { get; } + + bool IsLoadingData { get; } + + Task LoadInitialDataAsync(); + + Task CreateAndSaveReportAsync(ReportRequestDto request); + } +} diff --git a/FinTrack/Services/Reports/ReportStore.cs b/FinTrack/Services/Reports/ReportStore.cs new file mode 100644 index 0000000..0446760 --- /dev/null +++ b/FinTrack/Services/Reports/ReportStore.cs @@ -0,0 +1,125 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using FinTrackForWindows.Dtos.AccountDtos; +using FinTrackForWindows.Dtos.CategoryDtos; +using FinTrackForWindows.Dtos.ReportDtos; +using FinTrackForWindows.Enums; +using FinTrackForWindows.Helpers; +using FinTrackForWindows.Models.Report; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; + +namespace FinTrackForWindows.Services.Reports +{ + public partial class ReportStore : ObservableObject, IReportStore + { + private readonly IApiService _apiService; + private readonly ILogger _logger; + + public ObservableCollection AvailableReportTypes { get; } + public ObservableCollection AvailableAccounts { get; } + public ObservableCollection AvailableCategories { get; } + public ObservableCollection SortingCriteria { get; } + public ObservableCollection AvailableDocumentFormats { get; } + + [ObservableProperty] + private bool _isLoadingData; + + public ReportStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + + AvailableReportTypes = new(Enum.GetValues(typeof(ReportType)).Cast()); + AvailableAccounts = new(); + AvailableCategories = new(); + SortingCriteria = new(); + AvailableDocumentFormats = new(Enum.GetValues(typeof(DocumentFormat)).Cast()); + } + + public async Task LoadInitialDataAsync() + { + if (IsLoadingData) return; + + if (AvailableAccounts.Any() || AvailableCategories.Any()) return; + + IsLoadingData = true; + try + { + _logger.LogInformation("Loading initial data for ReportStore..."); + + var accountsTask = _apiService.GetAsync>("Account"); + var categoriesTask = _apiService.GetAsync>("categories"); + + await Task.WhenAll(accountsTask, categoriesTask); + + var accountsFromApi = await accountsTask; + var categoriesFromApi = await categoriesTask; + + AvailableAccounts.Clear(); + if (accountsFromApi != null) + { + foreach (var acc in accountsFromApi) + { + AvailableAccounts.Add(new SelectableOptionReport(acc.Id, acc.Name)); + } + } + + AvailableCategories.Clear(); + if (categoriesFromApi != null) + { + foreach (var cat in categoriesFromApi) + { + AvailableCategories.Add(new SelectableOptionReport(cat.Id, cat.Name)); + } + } + + SortingCriteria.Clear(); + SortingCriteria.Add("By Date (Newest to Oldest)"); + SortingCriteria.Add("By Date (Oldest to Newest)"); + SortingCriteria.Add("By Amount (Highest to Lowest)"); + SortingCriteria.Add("By Amount (Lowest to Highest)"); + + _logger.LogInformation("Initial data for ReportStore loaded successfully."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load initial data for ReportStore."); + throw; + } + finally + { + IsLoadingData = false; + } + } + + public async Task CreateAndSaveReportAsync(ReportRequestDto request) + { + _logger.LogInformation("Sending report creation request. Type: {ReportType}", request.ReportType); + + try + { + var result = await _apiService.PostAndDownloadReportAsync("Reports/generate", request); + + if (result.HasValue && result.Value.FileBytes.Length > 0) + { + var (fileBytes, fileName) = result.Value; + string savedPath = await FileSaver.SaveReportToDocumentsAsync(fileBytes, fileName); + _logger.LogInformation("Report saved successfully: {Path}", savedPath); + + return savedPath; + } + else + { + _logger.LogWarning("No file data received from API or no data found for the report."); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while creating and saving the report."); + return null; + } + } + } +} \ No newline at end of file diff --git a/FinTrack/Services/Transactions/TransactionStore.cs b/FinTrack/Services/Transactions/TransactionStore.cs index 89d6245..1dd2f70 100644 --- a/FinTrack/Services/Transactions/TransactionStore.cs +++ b/FinTrack/Services/Transactions/TransactionStore.cs @@ -64,7 +64,7 @@ public async Task LoadTransactionsAsync() { if (_transactions.Any()) { - _logger.LogInformation("İşlemler zaten yüklü. API çağrısı atlanıyor."); + _logger.LogInformation("Transactions are already loaded. Skipping API call."); return; } @@ -89,11 +89,11 @@ public async Task LoadTransactionsAsync() Currency = dto.Currency }); } - _logger.LogInformation("{Count} adet işlem Transactionstore'a yüklendi.", _transactions.Count); + _logger.LogInformation("{Count} transactions loaded into TransactionStore.", _transactions.Count); } catch (Exception ex) { - _logger.LogError(ex, "Transactionstore'da işlemler yüklenirken hata oluştu."); + _logger.LogError(ex, "An error occurred while loading transactions in TransactionStore."); } } diff --git a/FinTrack/Services/Users/IUserStore.cs b/FinTrack/Services/Users/IUserStore.cs new file mode 100644 index 0000000..547e2bc --- /dev/null +++ b/FinTrack/Services/Users/IUserStore.cs @@ -0,0 +1,14 @@ +using FinTrackForWindows.Models.User; + +namespace FinTrackForWindows.Services.Users +{ + public interface IUserStore + { + UserModel? CurrentUser { get; } + event Action? UserChanged; + Task LoadCurrentUserAsync(); + Task UpdateUserAsync(UserModel updatedUserData); + + void ClearCurrentUser(); + } +} diff --git a/FinTrack/Services/Users/UserStore.cs b/FinTrack/Services/Users/UserStore.cs new file mode 100644 index 0000000..9ce1e96 --- /dev/null +++ b/FinTrack/Services/Users/UserStore.cs @@ -0,0 +1,102 @@ +using FinTrackForWindows.Dtos; +using FinTrackForWindows.Models.User; +using FinTrackForWindows.Services.Api; +using Microsoft.Extensions.Logging; + +namespace FinTrackForWindows.Services.Users +{ + public class UserStore : IUserStore + { + private readonly IApiService _apiService; + private readonly ILogger _logger; + private UserModel? _currentUser; + + public UserModel? CurrentUser + { + get => _currentUser; + private set + { + if (_currentUser != value) + { + _currentUser = value; + UserChanged?.Invoke(); + } + } + } + + public event Action? UserChanged; + + public UserStore(IApiService apiService, ILogger logger) + { + _apiService = apiService; + _logger = logger; + } + + public async Task LoadCurrentUserAsync() + { + _logger.LogInformation("Mevcut kullanıcı bilgileri yükleniyor..."); + try + { + var userDto = await _apiService.GetAsync("User"); + var user = new UserModel + { + Id = userDto.Id, + UserName = userDto.UserName, + Email = userDto.Email, + ProfilePictureUrl = userDto.ProfilePicture ?? string.Empty, + MembershipPlan = userDto.MembershipType, + }; + if (userDto != null) + { + CurrentUser = user; + _logger.LogInformation("Kullanıcı bilgileri başarıyla yüklendi: {UserName}", userDto.UserName); + } + else + { + _logger.LogWarning("API'den kullanıcı bilgileri alınamadı veya kullanıcı bulunamadı."); + ClearCurrentUser(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Kullanıcı bilgileri yüklenirken bir hata oluştu."); + ClearCurrentUser(); + } + } + + public async Task UpdateUserAsync(UserModel updatedUserData) + { + //_logger.LogInformation("Kullanıcı bilgileri güncelleniyor: {UserName}", updatedUserData.UserName); + //try + //{ + // var updatedUserFromApi = await _apiService.PutAsync("User", updatedUserData); + + // if (updatedUserFromApi != null) + // { + // CurrentUser = updatedUserFromApi; + // _logger.LogInformation("Kullanıcı bilgileri başarıyla güncellendi."); + // return true; + // } + + // _logger.LogWarning("Kullanıcı güncelleme isteği API tarafından başarısız oldu veya geçersiz veri döndü."); + // return false; + //} + //catch (Exception ex) + //{ + // _logger.LogError(ex, "Kullanıcı bilgileri güncellenirken bir hata oluştu."); + // return false; + //} + await Task.Delay(1000); + return false; + } + + public void ClearCurrentUser() + { + if (CurrentUser != null) + { + _logger.LogInformation("Mevcut kullanıcı bilgileri temizleniyor."); + CurrentUser = null; + } + } + } +} diff --git a/FinTrack/Styles/ModernStyles.xaml b/FinTrack/Styles/ModernStyles.xaml index f8d5453..e7dcce7 100644 --- a/FinTrack/Styles/ModernStyles.xaml +++ b/FinTrack/Styles/ModernStyles.xaml @@ -4,16 +4,17 @@ - + - + - + + #FF58A6FF @@ -26,6 +27,8 @@ + + @@ -543,6 +546,93 @@ + + + + + + + + + + + + + + + + - - - + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FinTrack/ViewModels/AccountViewModel.cs b/FinTrack/ViewModels/AccountViewModel.cs index 9604c16..a2c99fc 100644 --- a/FinTrack/ViewModels/AccountViewModel.cs +++ b/FinTrack/ViewModels/AccountViewModel.cs @@ -71,7 +71,6 @@ public AccountViewModel(ILogger logger, IApiService apiService FilteredAccounts = new ObservableCollection(); _accountStore.AccountsChanged += (s, e) => ApplyFilters(); - // Sadece InitializeViewModel'i çağırarak çift yüklemeyi önle _ = InitializeViewModel(); PrepareForNewAccount(); diff --git a/FinTrack/ViewModels/AuthenticatorViewModel.cs b/FinTrack/ViewModels/AuthenticatorViewModel.cs index 19157e8..1a5424d 100644 --- a/FinTrack/ViewModels/AuthenticatorViewModel.cs +++ b/FinTrack/ViewModels/AuthenticatorViewModel.cs @@ -2,8 +2,8 @@ using CommunityToolkit.Mvvm.Messaging; using FinTrackForWindows.Core; using FinTrackForWindows.Messages; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -21,6 +21,7 @@ public partial class AuthenticatorViewModel : ObservableObject private readonly ISecureTokenStorage _secureToken; private readonly ILogger _logger; + private readonly IAppInNotificationService _appInNotificationService; public AuthenticatorViewModel( LoginViewModel loginViewModel, @@ -29,7 +30,8 @@ public AuthenticatorViewModel( ForgotPasswordViewModel forgotPasswordViewModel, ApplicationRecognizeSlideViewModel applicationRecognizeSlideViewModel, ISecureTokenStorage secureToken, - ILogger logger) + ILogger logger, + IAppInNotificationService appInNotificationService) { _loginViewModel = loginViewModel; _registerViewModel = registerViewModel; @@ -37,6 +39,7 @@ public AuthenticatorViewModel( _forgotPasswordViewModel = forgotPasswordViewModel; _applicationRecognizeSlideViewModel = applicationRecognizeSlideViewModel; _currentViewModel = _applicationRecognizeSlideViewModel; + _appInNotificationService = appInNotificationService; _applicationRecognizeSlideViewModel.NavigateToLoginRequested += () => CurrentViewModel = _loginViewModel; @@ -74,19 +77,21 @@ private void SavedTokenLogin() bool isValid = TokenValidator.IsTokenValid(token); if (!isValid) { - MessageBox.Show("Token geçersiz. Lütfen tekrar giriş yapın.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Geçersiz token bulundu. Kullanıcıdan yeni giriş yapması istendi."); + _appInNotificationService.ShowWarning("Invalid token. Please log in again."); + _logger.LogWarning("Invalid token found. User is required to log in again."); SessionManager.ClearToken(); _secureToken.ClearToken(); } - - SessionManager.SetToken(token); - _logger.LogInformation("Kullanıcı zaten giriş yapmış. Token kullanıldı."); - WeakReferenceMessenger.Default.Send(new LoginSuccessMessage()); + else + { + SessionManager.SetToken(token); + _logger.LogInformation("User already logged in. Token used."); + WeakReferenceMessenger.Default.Send(new LoginSuccessMessage()); + } } else { - _logger.LogInformation("Kullanıcı henüz giriş yapmamış. Uygulama tanıtım slaytları gösteriliyor."); + _logger.LogInformation("User has not logged in yet. Application introduction slides are being shown."); } } } diff --git a/FinTrack/ViewModels/BottomBarViewModel.cs b/FinTrack/ViewModels/BottomBarViewModel.cs index 24f4797..62b85c8 100644 --- a/FinTrack/ViewModels/BottomBarViewModel.cs +++ b/FinTrack/ViewModels/BottomBarViewModel.cs @@ -27,14 +27,14 @@ public BottomBarViewModel(ILogger logger) Company = $"© {year} FinTrack Inc."; Version = "v1.0.0"; - LastSyncStatus = "Son Senkronizasyon: Başarılı"; + LastSyncStatus = "Last Synchronization: Successful"; } [RelayCommand] private void AddNewTransaction() { - _logger.LogInformation("Yeni işlem ekleniyor..."); - MessageBox.Show("Yeni işlem ekleme özelliği henüz uygulanmadı.", "Bilgi", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation("Adding new transaction..."); + MessageBox.Show("The add new transaction feature is not implemented yet.", "Information", MessageBoxButton.OK, MessageBoxImage.Information); } } } diff --git a/FinTrack/ViewModels/BudgetViewModel.cs b/FinTrack/ViewModels/BudgetViewModel.cs index ef1a6e1..806f7d9 100644 --- a/FinTrack/ViewModels/BudgetViewModel.cs +++ b/FinTrack/ViewModels/BudgetViewModel.cs @@ -5,6 +5,7 @@ using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Budget; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; using FinTrackForWindows.Services.Budgets; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; @@ -14,7 +15,7 @@ namespace FinTrackForWindows.ViewModels { public partial class BudgetViewModel : ObservableObject { - private const string AllCategoriesFilter = "Tüm Kategoriler"; + private const string AllCategoriesFilter = "All Categories"; public ReadOnlyObservableCollection Budgets => _budgetStore.Budgets; public ObservableCollection FilteredBudgets { get; } @@ -27,38 +28,42 @@ public partial class BudgetViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(SaveButtonText))] private bool isEditing; - // Filtreleme Özellikleri [ObservableProperty] - private string? filterByName; - [ObservableProperty] - private string? filterByMinAmount; - [ObservableProperty] - private string? filterByMaxAmount; - [ObservableProperty] - private string? selectedFilterCategory; + [NotifyPropertyChangedFor(nameof(HasFilteredBudgets))] + private bool isFilterActive; + + [ObservableProperty] private string? filterByName; + [ObservableProperty] private string? filterByMinAmount; + [ObservableProperty] private string? filterByMaxAmount; + [ObservableProperty] private string? selectedFilterCategory; + + public bool HasFilteredBudgets => FilteredBudgets.Any(); - // Kategori listeleri public ObservableCollection CategoriesForForm { get; } public ObservableCollection CategoriesForFilter { 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; + private readonly IAppInNotificationService _notificationService; - public BudgetViewModel(IBudgetStore budgetStore, ILogger logger, IApiService apiService) + public BudgetViewModel(IBudgetStore budgetStore, ILogger logger, IApiService apiService, IAppInNotificationService appInNotificationService) { _budgetStore = budgetStore; _logger = logger; _apiService = apiService; + _notificationService = appInNotificationService; FilteredBudgets = new ObservableCollection(); CategoriesForForm = new ObservableCollection(); CategoriesForFilter = new ObservableCollection(); + FilteredBudgets.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasFilteredBudgets)); + _budgetStore.BudgetsChanged += (s, e) => ApplyFilters(); _ = InitializeViewModelAsync(); @@ -68,16 +73,8 @@ private async Task InitializeViewModelAsync() { await LoadCategoriesAsync(); await _budgetStore.LoadBudgetsAsync(); - ApplyFilters(); - if (!FilteredBudgets.Any()) - { - PrepareForNewBudget(); - } - else - { - SelectedBudget = FilteredBudgets.FirstOrDefault(); - } + PrepareForNewBudget(); } private async Task LoadCategoriesAsync() @@ -91,7 +88,6 @@ private async Task LoadCategoriesAsync() { CategoriesForForm.Clear(); CategoriesForFilter.Clear(); - CategoriesForFilter.Add(AllCategoriesFilter); foreach (var category in categoriesFromApi.OrderBy(c => c.Name)) @@ -99,14 +95,13 @@ private async Task LoadCategoriesAsync() CategoriesForForm.Add(category.Name); CategoriesForFilter.Add(category.Name); } - SelectedFilterCategory = AllCategoriesFilter; }); } catch (Exception ex) { - _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); + _logger.LogError(ex, "Kategoriler yüklenirken bir hata oluştu."); + _notificationService.ShowError("Kategoriler yüklenemedi. Lütfen internet bağlantınızı kontrol edin."); } } @@ -117,52 +112,49 @@ private async Task LoadCategoriesAsync() private void ApplyFilters() { - var previouslySelectedId = SelectedBudget?.Id; - IEnumerable filtered = _budgetStore.Budgets; + bool activeFilter = false; if (!string.IsNullOrWhiteSpace(FilterByName)) { filtered = filtered.Where(b => b.Name.Contains(FilterByName, StringComparison.OrdinalIgnoreCase)); + activeFilter = true; } if (!string.IsNullOrEmpty(SelectedFilterCategory) && SelectedFilterCategory != AllCategoriesFilter) { filtered = filtered.Where(b => b.Category.Equals(SelectedFilterCategory, StringComparison.OrdinalIgnoreCase)); + activeFilter = true; } if (decimal.TryParse(FilterByMinAmount, out var minAmount)) { filtered = filtered.Where(b => b.AllocatedAmount >= minAmount); + activeFilter = true; } if (decimal.TryParse(FilterByMaxAmount, out var maxAmount) && maxAmount > 0) { filtered = filtered.Where(b => b.AllocatedAmount <= maxAmount); + activeFilter = true; } + IsFilterActive = activeFilter; + FilteredBudgets.Clear(); foreach (var budget in filtered.OrderByDescending(b => b.StartDate)) { FilteredBudgets.Add(budget); } + } - if (previouslySelectedId.HasValue) - { - var reselectBudget = FilteredBudgets.FirstOrDefault(b => b.Id == previouslySelectedId.Value); - if (reselectBudget != null) - { - SelectedBudget = reselectBudget; - } - else if (!FilteredBudgets.Any()) - { - PrepareForNewBudget(); - } - else - { - SelectedBudget = FilteredBudgets.FirstOrDefault(); - } - } + [RelayCommand] + private void ClearFilters() + { + FilterByName = string.Empty; + FilterByMinAmount = string.Empty; + FilterByMaxAmount = string.Empty; + SelectedFilterCategory = AllCategoriesFilter; } [RelayCommand] @@ -170,23 +162,30 @@ private async Task SaveBudgetAsync() { if (SelectedBudget == null || string.IsNullOrWhiteSpace(SelectedBudget.Name) || SelectedBudget.AllocatedAmount <= 0) { - MessageBox.Show("Lütfen bütçe adı ve sıfırdan büyük bir miktar girin.", "Eksik Bilgi", MessageBoxButton.OK, MessageBoxImage.Warning); + _notificationService.ShowWarning("Bütçe adı ve tutarı boş veya sıfırdan küçük olamaz."); 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); + _notificationService.ShowWarning("Lütfen bir kategori seçin veya yazın."); return; } + if (!CategoriesForForm.Contains(categoryToSave)) + { + CategoriesForForm.Add(categoryToSave); + CategoriesForFilter.Add(categoryToSave); + } + var budgetDto = new BudgetCreateDto { Name = SelectedBudget.Name, Description = SelectedBudget.Description, Category = categoryToSave, AllocatedAmount = SelectedBudget.AllocatedAmount, + ReachedAmount = SelectedBudget.ReachedAmount, Currency = SelectedBudget.Currency, StartDate = SelectedBudget.StartDate, EndDate = SelectedBudget.EndDate, @@ -204,13 +203,12 @@ private async Task SaveBudgetAsync() await _budgetStore.AddBudgetAsync(budgetDto); } - await LoadCategoriesAsync(); PrepareForNewBudget(); } catch (Exception ex) { - _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); + _logger.LogError(ex, "Bütçe kaydedilirken bir hata oluştu."); + _notificationService.ShowError("Bütçe kaydedilemedi. Lütfen internet bağlantınızı kontrol edin."); } } @@ -233,8 +231,8 @@ private async Task DeleteBudgetAsync(BudgetModel? budgetToDelete) } catch (Exception ex) { - _logger.LogError(ex, "Bütçe silinirken hata oluştu: {BudgetId}", budgetToDelete.Id); - MessageBox.Show("Bütçe silinemedi.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.LogError(ex, "Bütçe silinirken bir hata oluştu: {BudgetId}", budgetToDelete.Id); + _notificationService.ShowError("Bütçe silinemedi. Lütfen internet bağlantınızı kontrol edin."); } } @@ -250,6 +248,7 @@ private void PrepareToEditBudget(BudgetModel? budgetToEdit) Description = budgetToEdit.Description, Category = budgetToEdit.Category, AllocatedAmount = budgetToEdit.AllocatedAmount, + ReachedAmount = budgetToEdit.ReachedAmount, CurrentAmount = budgetToEdit.CurrentAmount, Currency = budgetToEdit.Currency, StartDate = budgetToEdit.StartDate, @@ -259,15 +258,58 @@ private void PrepareToEditBudget(BudgetModel? budgetToEdit) } [RelayCommand] - private void CleanForm() - { - PrepareForNewBudget(); - } - private void PrepareForNewBudget() { SelectedBudget = new BudgetModel(); IsEditing = false; } + + [RelayCommand] + private void StartEditReachedAmount(BudgetModel? budget) + { + if (budget == null) return; + + foreach (var b in FilteredBudgets) + { + if (b.IsEditingReachedAmount) + { + b.IsEditingReachedAmount = false; + } + } + + budget.IsEditingReachedAmount = true; + } + + [RelayCommand] + private async Task SaveReachedAmountAsync(BudgetModel? budget) + { + if (budget == null || !budget.IsEditingReachedAmount) return; + + budget.IsEditingReachedAmount = false; + + if (!budget.ReachedAmount.HasValue) + { + _notificationService.ShowWarning("Ulaşılan miktar geçerli bir değer olmalıdır."); + return; + } + + try + { + var dto = new BudgetUpdateReachedAmountDto + { + BudgetId = budget.Id, + ReachedAmount = budget.ReachedAmount.Value + }; + + await _budgetStore.UpdateReachedAmountAsync(dto); + + _notificationService.ShowSuccess($"'{budget.Name}' bütçesinin ulaşılan miktarı güncellendi."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ulaşılan miktar güncellenirken hata oluştu: {BudgetId}", budget.Id); + _notificationService.ShowError("Miktar güncellenemedi. Lütfen tekrar deneyin."); + } + } } } \ No newline at end of file diff --git a/FinTrack/ViewModels/CurrenciesViewModel.cs b/FinTrack/ViewModels/CurrenciesViewModel.cs index 9057761..0098957 100644 --- a/FinTrack/ViewModels/CurrenciesViewModel.cs +++ b/FinTrack/ViewModels/CurrenciesViewModel.cs @@ -118,7 +118,7 @@ private async Task InitializeDataAsync() } catch (Exception ex) { - _logger.LogError(ex, "Para birimi verileri başlatılırken hata oluştu."); + _logger.LogError(ex, "An error occurred while initializing currency data."); InitializeEmptyChart("Failed to load currency list."); } } @@ -137,14 +137,14 @@ private async Task LoadHistoricalDataAsync(string targetCurrencyCode) return; } - _logger.LogInformation("{Target} için geçmiş veriler ViewModel'de işleniyor.", targetCurrencyCode); + _logger.LogInformation("Processing historical data for {Target} in ViewModel.", targetCurrencyCode); var dailyHistoricalRates = UpdateChart(historyData); UpdateAdditionalVisuals(dailyHistoricalRates); UpdateDetails(historyData.ChangeSummary); } catch (Exception ex) { - _logger.LogError(ex, "ViewModel'de geçmiş veriler işlenirken hata oluştu."); + _logger.LogError(ex, "An error occurred while processing historical data in ViewModel."); InitializeEmptyChart($"Error processing data for {targetCurrencyCode}."); UpdateDetails(null); } @@ -162,7 +162,7 @@ private async Task ChangePeriod(string? newPeriod) return; } - _logger.LogInformation("Grafik periyodu {NewPeriod} olarak değiştiriliyor.", newPeriod); + _logger.LogInformation("Changing chart period to {NewPeriod}.", newPeriod); SelectedPeriod = newPeriod; await LoadHistoricalDataAsync(SelectedCurrency.ToCurrencyCode); @@ -374,7 +374,7 @@ private void FilterCurrencies() c.ToCurrencyName.ToLowerInvariant().Contains(searchText)); FilteredCurrencies = new ObservableCollection(filtered); } - _logger.LogInformation("Para birimleri '{SearchText}' metnine göre filtrelendi.", CurrencySearch); + _logger.LogInformation("Currencies filtered by search text: '{SearchText}'.", CurrencySearch); } } } \ No newline at end of file diff --git a/FinTrack/ViewModels/DashboardViewModel.cs b/FinTrack/ViewModels/DashboardViewModel.cs index 7b6247b..1860168 100644 --- a/FinTrack/ViewModels/DashboardViewModel.cs +++ b/FinTrack/ViewModels/DashboardViewModel.cs @@ -7,10 +7,12 @@ using FinTrackForWindows.Models.Debt; using FinTrackForWindows.Models.Transaction; using FinTrackForWindows.Services.Accounts; +using FinTrackForWindows.Services.AppInNotifications; using FinTrackForWindows.Services.Budgets; using FinTrackForWindows.Services.Currencies; using FinTrackForWindows.Services.Debts; using FinTrackForWindows.Services.Memberships; +using FinTrackForWindows.Services.Reports; using FinTrackForWindows.Services.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -36,6 +38,15 @@ public partial class DashboardViewModel : ObservableObject [ObservableProperty] private string _totalBalance_DashboardView_TextBlock = string.Empty; + [ObservableProperty] + private string _totalBalanceIntegerPart = "0"; + + [ObservableProperty] + private string _totalBalanceFractionalPart = ",00"; + + [ObservableProperty] + private string _totalBalanceCurrency = "TRY"; + [ObservableProperty] private ObservableCollection? _transactions_DashboardView_ListView; @@ -64,6 +75,8 @@ public partial class DashboardViewModel : ObservableObject private readonly ICurrenciesStore _currenciesStore; private readonly IMembershipStore _membershipStore; private readonly IDebtStore _debtStore; + private readonly IReportStore _reportStore; + private readonly IAppInNotificationService _appInNotificationService; public IEnumerable DashboardBudgets => _budgetStore.Budgets.Take(4); public IEnumerable DashboardAccounts => _accountStore.Accounts.Take(2); @@ -87,7 +100,9 @@ public DashboardViewModel( ITransactionStore transactionStore, ICurrenciesStore currenciesStore, IMembershipStore membershipStore, - IDebtStore debtStore) + IDebtStore debtStore, + IReportStore reportStore, + IAppInNotificationService appInNotificationService) { _logger = logger; _serviceProvider = serviceProvider; @@ -97,6 +112,8 @@ public DashboardViewModel( _currenciesStore = currenciesStore; _membershipStore = membershipStore; _debtStore = debtStore; + _reportStore = reportStore; + _appInNotificationService = appInNotificationService; _budgetStore.BudgetsChanged += OnBudgetsChanged; _accountStore.AccountsChanged += OnAccountsChanged; @@ -127,7 +144,6 @@ await Task.WhenAll( RefreshDashboardTransactions(); RefreshDashboardCurrencies(); RefreshDashboardMembership(); - RefreshDashboardDebts(); LoadReportData(); @@ -145,7 +161,7 @@ private void RefreshDashboardBudgets() 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ı.", + RemainingTime = $"Only {(budget.EndDate - budget.StartDate).Days} days left.", StatusBrush = (Brush)Application.Current.FindResource("StatusGreenBrush") }); } @@ -164,18 +180,62 @@ private void OnBudgetsChanged(object? sender, NotifyCollectionChangedEventArgs e private void RefreshDashboardAccounts() { - Accounts_DashboardView_ItemsControl = new ObservableCollection(); + var newAccountList = new ObservableCollection(); + _logger.LogInformation("Refreshing dashboard accounts..."); + + if (_transactionStore.Transactions == null || !_transactionStore.Transactions.Any()) + { + _logger.LogWarning("Transaction store is empty. Account income/expense ratios cannot be calculated."); + } foreach (var account in DashboardAccounts) { - Accounts_DashboardView_ItemsControl.Add(new AccountDashboard + var accountTransactions = _transactionStore.Transactions + .Where(t => t.AccountId == account.Id) + .ToList(); + + _logger.LogInformation("Account '{AccountName}' (ID: {AccountId}): Found {TransactionCount} transactions.", + account.Name, account.Id, accountTransactions.Count); + + decimal totalIncome = accountTransactions + .Where(t => t.Type == TransactionType.Income) + .Sum(t => t.Amount); + + decimal totalExpense = accountTransactions + .Where(t => t.Type == TransactionType.Expense) + .Sum(t => t.Amount); + + decimal grandTotal = totalIncome + totalExpense; + + double incomePercentage = 0; + double expensePercentage = 0; + + if (grandTotal > 0) + { + incomePercentage = (double)(totalIncome / grandTotal * 100); + expensePercentage = (double)(totalExpense / grandTotal * 100); + } + else if (account.Balance > 0 && !accountTransactions.Any()) + { + incomePercentage = 100; + } + + _logger.LogInformation("Account '{AccountName}': Income={Income}, Expense={Expense}, Income%={IncomePerc}, Expense%={ExpensePerc}", + account.Name, totalIncome, totalExpense, incomePercentage, expensePercentage); + + newAccountList.Add(new AccountDashboard { Name = account.Name, - Percentage = 70, - Balance = "Test", - ProgressBarBrush = (Brush)Application.Current.FindResource("StatusGreenBrush") + Balance = $"{account.Balance:N2} {account.Currency}", + IncomePercentage = incomePercentage, + ExpensePercentage = expensePercentage, + IncomeAmountText = $"+{totalIncome:N2}", + ExpenseAmountText = $"-{totalExpense:N2}" }); } + + Accounts_DashboardView_ItemsControl = newAccountList; + _logger.LogInformation("Dashboard accounts UI model updated."); } private void OnAccountsChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -193,7 +253,9 @@ private async Task CalculateTotalBalance() { if (_accountStore.Accounts == null || !_accountStore.Accounts.Any()) { - TotalBalance_DashboardView_TextBlock = "0.00 TRY"; + TotalBalanceIntegerPart = "0"; + TotalBalanceFractionalPart = ",00"; + TotalBalanceCurrency = "TRY"; return; } @@ -201,14 +263,11 @@ private async Task CalculateTotalBalance() 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; @@ -220,11 +279,25 @@ private async Task CalculateTotalBalance() ); } } - var convertedAmounts = await Task.WhenAll(conversionTasks); + var convertedAmounts = await Task.WhenAll(conversionTasks); totalBalanceInTargetCurrency += convertedAmounts.Sum(); - TotalBalance_DashboardView_TextBlock = $"{totalBalanceInTargetCurrency:N2} {targetCurrency}"; + string formattedBalance = totalBalanceInTargetCurrency.ToString("N2"); + + var parts = formattedBalance.Split(','); + if (parts.Length == 2) + { + TotalBalanceIntegerPart = parts[0]; + TotalBalanceFractionalPart = "," + parts[1]; + } + else + { + TotalBalanceIntegerPart = formattedBalance; + TotalBalanceFractionalPart = ",00"; + } + + TotalBalanceCurrency = targetCurrency; } // ------ @@ -262,10 +335,10 @@ private void RefreshDashboardTransactions() 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() ?? ""}"; + $"A total of {Transactions_DashboardView_ListView.Count} transactions found. " + + $"Income: +{totalIncome} {Transactions_DashboardView_ListView.FirstOrDefault()?.Amount?.Split(' ')?.LastOrDefault() ?? ""}, " + + $"Expense: -{totalExpense} {Transactions_DashboardView_ListView.FirstOrDefault()?.Amount?.Split(' ')?.LastOrDefault() ?? ""} " + + $"Remaining: {remainingBalance} {Transactions_DashboardView_ListView.FirstOrDefault()?.Amount?.Split(' ')?.LastOrDefault() ?? ""}"; } private void OnTransactionsChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -355,28 +428,11 @@ private void OnMembershipChanged() // ------ - private void RefreshDashboardDebts() - { - CurrentDebt_DashboardView_Multiple = new DebtDashboard - { - LenderName = "Ali Veli", - LenderIconPath = "https://i.pinimg.com/236x/be/a3/49/bea3491915571d34a026753f4a872000.jpg", - BorrowerName = "Ahmet Mehmet", - BorrowerIconPath = "https://pbs.twimg.com/profile_images/1144861916734451712/D76C3ugh_400x400.jpg", - Status = "Ödenmemiş", - StatusBrush = (Brush)Application.Current.FindResource("StatusGreenBrush"), - Amount = "1.000$", - CreationDate = "01.01.2025", - DueDate = "01.02.2025", - ReviewDate = "01.03.2025" - }; - } - private void OnDebtsChanged() { App.Current.Dispatcher.Invoke(() => { - _logger.LogInformation("DebtStore değişti, Dashboard borçları yenileniyor."); + _logger.LogInformation("DebtStore has changed, Dashboard debts are being renewed."); SetupDebtCarousel(); }); } @@ -392,8 +448,10 @@ private void SetupDebtCarousel() if (_debtStore.ActiveDebts.Count > 1) { - _debtCarouselTimer = new DispatcherTimer(); - _debtCarouselTimer.Interval = TimeSpan.FromSeconds(2); + _debtCarouselTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(5) + }; _debtCarouselTimer.Tick += DebtCarouselTimer_Tick; _debtCarouselTimer.Start(); } @@ -428,19 +486,19 @@ private void LoadReportData() { 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") + CreateReport("2025 Yılı Hesap Raporu", ReportType.Account), + CreateReport("2025 Yılı Gelir-Gider Raporu", ReportType.Transaction), + CreateReport("2025 Yılı Bütçe Raporu", ReportType.Budget), }; } - private ReportDashboardModel CreateReport(string name) + private ReportDashboardModel CreateReport(string name, ReportType type) { var reportLogger = _serviceProvider.GetRequiredService>(); - var report = new ReportDashboardModel(reportLogger) + var report = new ReportDashboardModel(reportLogger, _reportStore) { Name = name, + Type = type }; return report; } diff --git a/FinTrack/ViewModels/DebtViewModel.cs b/FinTrack/ViewModels/DebtViewModel.cs index f483e28..413fb1b 100644 --- a/FinTrack/ViewModels/DebtViewModel.cs +++ b/FinTrack/ViewModels/DebtViewModel.cs @@ -1,11 +1,14 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Debt; +using FinTrackForWindows.Services.AppInNotifications; using FinTrackForWindows.Services.Camera; using FinTrackForWindows.Services.Debts; using FinTrackForWindows.Views; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; using System.IO; using System.Windows; @@ -16,6 +19,8 @@ public partial class DebtViewModel : ObservableObject private readonly ILogger _logger; private readonly IDebtStore _debtStore; + private readonly IAppInNotificationService _appInNotificationService; + private readonly IServiceProvider _serviceProvider; public IDebtStore DebtStore => _debtStore; @@ -26,17 +31,28 @@ public partial class DebtViewModel : ObservableObject [ObservableProperty] private decimal newProposalAmount; + [ObservableProperty] + private BaseCurrencyType newCurrencyType; + + public ObservableCollection CurrencyTypes { get; } + [ObservableProperty] private string? newProposalDescription; [ObservableProperty] private DateTime newProposalDueDate = DateTime.Now.AddMonths(1); - public DebtViewModel(ILogger logger, IDebtStore debtStore, IServiceProvider serviceProvider) + public DebtViewModel(ILogger logger, + IDebtStore debtStore, + IServiceProvider serviceProvider, + IAppInNotificationService appInNotificationService) { _logger = logger; _debtStore = debtStore; _serviceProvider = serviceProvider; + _appInNotificationService = appInNotificationService; + + CurrencyTypes = new ObservableCollection(Enum.GetValues(typeof(BaseCurrencyType)).Cast()); } [RelayCommand] @@ -44,25 +60,27 @@ private async Task SendOfferAsync() { if (string.IsNullOrWhiteSpace(NewProposalBorrowerEmail) || NewProposalAmount <= 0) { - MessageBox.Show("Lütfen borçlu e-postası ve geçerli bir miktar girin.", "Doğrulama Hatası", MessageBoxButton.OK, MessageBoxImage.Warning); + _appInNotificationService.ShowWarning("Please enter a valid borrower email and amount."); return; } try { - await _debtStore.SendOfferAsync(NewProposalBorrowerEmail, NewProposalAmount, "TRY", NewProposalDueDate, NewProposalDescription); + await _debtStore.SendOfferAsync(NewProposalBorrowerEmail, NewProposalAmount, NewCurrencyType, NewProposalDueDate, NewProposalDescription); NewProposalBorrowerEmail = string.Empty; NewProposalAmount = 0; NewProposalDescription = string.Empty; NewProposalDueDate = DateTime.Now.AddMonths(1); - MessageBox.Show("Teklif başarıyla gönderildi.", "Başarılı", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation("Debt offer sent successfully from ViewModel."); + _appInNotificationService.ShowSuccess("Offer sent successfully."); } catch (Exception ex) { _logger.LogError(ex, "Failed to send debt offer from ViewModel."); - MessageBox.Show("Teklif gönderilirken bir hata oluştu.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while sending the offer.", ex); + return; } } @@ -78,7 +96,8 @@ private async Task RespondToOfferAsync(object parameter) catch (Exception ex) { _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); + _appInNotificationService.ShowError("An error occurred while responding to the offer.", ex); + return; } } @@ -109,7 +128,7 @@ private async Task UploadVideoAsync(DebtModel? debt) } }; - var videoRecorderViewModel = new VideoRecorderViewModel(debt, cameraService, closeAction); + var videoRecorderViewModel = new VideoRecorderViewModel(debt, cameraService, closeAction, _logger, _appInNotificationService); var videoRecorderWindow = new VideoRecorderWindow { DataContext = videoRecorderViewModel @@ -122,16 +141,16 @@ private async Task UploadVideoAsync(DebtModel? debt) try { await _debtStore.UploadVideoAsync(debt, videoPathToProcess); - MessageBox.Show("Video başarıyla yüklendi.", "Başarılı", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation("Video uploaded successfully from ViewModel for DebtId: {DebtId}", debt.Id); + _appInNotificationService.ShowSuccess("Video uploaded successfully."); } catch (Exception ex) { _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); + _appInNotificationService.ShowError("An error occurred while uploading the video.", ex); } finally { - // Yükleme başarılı da olsa başarısız da olsa geçici dosyayı SİLİNECEK. try { File.Delete(videoPathToProcess); @@ -140,12 +159,12 @@ private async Task UploadVideoAsync(DebtModel? debt) catch (Exception fileEx) { _logger.LogWarning(fileEx, "Could not delete temporary video file: {FilePath}", videoPathToProcess); + _appInNotificationService.ShowWarning("Temporary video file could not be deleted. Please try deleting it manually.", fileEx); } } } else if (!string.IsNullOrEmpty(videoPathToProcess)) { - // Kullanıcı "Cancel" ettiyse kaydedilmiş ama gönderilmemiş geçici dosyayı sil. try { File.Delete(videoPathToProcess); @@ -174,12 +193,14 @@ private async Task MarkAsDefaultedAsync(DebtModel? debt) try { await _debtStore.MarkDebtAsDefaultedAsync(debt.Id); - MessageBox.Show("The debt has been successfully marked as defaulted.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation("Debt marked as defaulted successfully for DebtId: {DebtId}", debt.Id); + _appInNotificationService.ShowSuccess("Debt marked as defaulted successfully."); } catch (Exception ex) { _logger.LogError(ex, "Failed to execute MarkAsDefaulted command for DebtId: {DebtId}", debt.Id); - MessageBox.Show("An error occurred while marking the debt as defaulted.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while marking the debt as defaulted.", ex); + return; } } } @@ -212,13 +233,14 @@ private async Task ViewVideoAsync(DebtModel? debt) } else { - MessageBox.Show("Could not retrieve the video. Please check the key and your permissions.", "Access Denied", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.LogWarning("Failed to retrieve video stream for DebtId: {DebtId} with provided key.", debt.Id); + _appInNotificationService.ShowWarning("Could not retrieve video stream. Please check the key and your permissions."); } } catch (Exception ex) { _logger.LogError(ex, "An error occurred while trying to view the video for DebtId: {DebtId}", debt.Id); - MessageBox.Show("An error occurred while trying to view the video.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while viewing the video.", ex); } } } diff --git a/FinTrack/ViewModels/FeedbackViewModel.cs b/FinTrack/ViewModels/FeedbackViewModel.cs index 68e1d03..46c6f56 100644 --- a/FinTrack/ViewModels/FeedbackViewModel.cs +++ b/FinTrack/ViewModels/FeedbackViewModel.cs @@ -3,10 +3,10 @@ using FinTrackForWindows.Dtos.FeedbackDtos; using FinTrackForWindows.Enums; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; using Microsoft.Win32; using System.Diagnostics; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -34,10 +34,13 @@ public partial class FeedbackViewModel : ObservableObject private readonly IApiService _apiService; - public FeedbackViewModel(ILogger logger, IApiService apiService) + private readonly IAppInNotificationService _appInNotificationService; + + public FeedbackViewModel(ILogger logger, IApiService apiService, IAppInNotificationService appInNotificationService) { _logger = logger; _apiService = apiService; + _appInNotificationService = appInNotificationService; FeedbackTypes = Enum.GetValues(typeof(FeedbackType)).Cast(); SelectedFeedbackType = FeedbackTypes.FirstOrDefault(); @@ -58,7 +61,7 @@ private async Task SendFeedback() await _apiService.PostAsync("Feedback", newFeedback); - // TODO: Burada sisteme bir e-posta göndermekte fayda var... + // TODO: It may be useful to send an email to the system here... _logger.LogInformation("Feedback submitted: Subject: {Subject}, Type: {Type}, Description: {Description}, File Path: {FilePath}", newFeedback.Subject, newFeedback.Type, newFeedback.Description, newFeedback.SavedFilePath); @@ -85,7 +88,7 @@ private void BrowseFile() if (openFileDialog.ShowDialog() == true) { SelectedFilePath = openFileDialog.FileName; - _logger.LogInformation("Seçilen dosya: {FilePath}", SelectedFilePath); + _logger.LogInformation("Selected file: {FilePath}", SelectedFilePath); } } @@ -100,8 +103,8 @@ private void OpenLink(string? url) } catch (Exception ex) { - MessageBox.Show($"Link açılamadı: {ex.Message}", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogError(ex, "Link açma hatası: {Url}", url); + _appInNotificationService.ShowError("The link could not be opened.", ex); + _logger.LogError(ex, "Error opening link: {Url}", url); } } diff --git a/FinTrack/ViewModels/FinBotViewModel.cs b/FinTrack/ViewModels/FinBotViewModel.cs index 70a703b..c28c707 100644 --- a/FinTrack/ViewModels/FinBotViewModel.cs +++ b/FinTrack/ViewModels/FinBotViewModel.cs @@ -6,7 +6,6 @@ using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; -using System.Text; namespace FinTrackForWindows.ViewModels { @@ -16,14 +15,14 @@ public partial class FinBotViewModel : ObservableObject [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SendMessageCommand))] - private string messageInput = string.Empty; + private string _messageInput = string.Empty; - private readonly ILogger _logger; + [ObservableProperty] + private bool _isAwaitingBotResponse = false; + private readonly ILogger _logger; private readonly IApiService _apiService; - - private const string chars = "qQwWeErRtTyYuUiIoOpPaAsSdDfFgGhHjJkKlLzZxXcCvVbBnNmM0123456789+-*/|<>£&()='!#${[]}"; - private readonly string? clientChatSessionId; + private readonly string _clientChatSessionId; public FinBotViewModel(ILogger logger, IApiService apiService) { @@ -31,13 +30,10 @@ public FinBotViewModel(ILogger logger, IApiService apiService) _apiService = apiService; Messages = new ObservableCollection(); + const string chars = "qQwWeErRtTyYuUiIoOpPaAsSdDfFgGhHjJkKlLzZxXcCvVbBnNmM0123456789"; Random random = new Random(); - StringBuilder result = new StringBuilder(10); - for (int i = 0; i < 10; i++) - { - result.Append(chars[random.Next(chars.Length)]); - } - clientChatSessionId = result.ToString(); + _clientChatSessionId = new string(Enumerable.Repeat(chars, 10) + .Select(s => s[random.Next(s.Length)]).ToArray()); LoadInitialMessage(); } @@ -50,85 +46,89 @@ private void LoadInitialMessage() { "List All My Accounts", "List All My Budgets", - "List All My Transacitons", + "List All My Transactions", "Total Income Amount", "Total Expense Amount" } }; Messages.Add(initialMessage); - _logger.LogInformation("Initial message loaded: {MessageText}", initialMessage.Text); } + private bool CanSendMessage() => !string.IsNullOrWhiteSpace(MessageInput) && !IsAwaitingBotResponse; + [RelayCommand(CanExecute = nameof(CanSendMessage))] private async Task SendMessageAsync() { - if (!CanSendMessage) return; + if (!CanSendMessage()) return; - var userMessage = new ChatMessageModel(MessageInput, MessageAuthor.User); + var userMessageText = MessageInput; + var userMessage = new ChatMessageModel(userMessageText, MessageAuthor.User); Messages.Add(userMessage); - MessageInput = string.Empty; - _logger.LogInformation("Kullanıcı mesaj yazdı. {MessageText}", userMessage.Text); - - var chatResponse = await _apiService.PostAsync("Chat/send", new ChatRequestDto - { - Message = userMessage.Text, - ClientChatSessionId = clientChatSessionId, - }); + _logger.LogInformation("User sent a message: {MessageText}", userMessage.Text); - if (chatResponse != null) - { - var botResponse = new ChatMessageModel(chatResponse.Reply ?? "N/A", MessageAuthor.Bot); - Messages.Add(botResponse); - } + await GetBotResponseAsync(userMessageText); } - private bool CanSendMessage => !string.IsNullOrWhiteSpace(MessageInput); - [RelayCommand] private async Task SendQuickActionAsync(string actionText) { - if (string.IsNullOrWhiteSpace(actionText)) return; + if (string.IsNullOrWhiteSpace(actionText) || IsAwaitingBotResponse) return; - string actionMessage = string.Empty; - switch (actionText) + string botCommandMessage = actionText switch { - case "List All My Accounts": - actionMessage = "List all my accounts. Don't give too long explanations."; - return; - case "List All My Budgets": - actionMessage = "List all my budgets. Don't give too long explanations."; - return; - case "List All My Transacitons": - actionMessage = "List all my transacitons. Don't give too long explanations."; - return; - case "Total Income Amount": - actionMessage = "The sum of all my income. Don't give too long explanations."; - return; - case "Total Expense Amount": - actionMessage = "The sum of all my expense. Don't give too long explanations."; - return; - } + "List All My Accounts" => "List all my accounts. Don't give too long explanations.", + "List All My Budgets" => "List all my budgets. Don't give too long explanations.", + "List All My Transactions" => "List all my transacitons. Don't give too long explanations.", + "Total Income Amount" => "The sum of all my income. Don't give too long explanations.", + "Total Expense Amount" => "The sum of all my expense. Don't give too long explanations.", + _ => actionText + }; - var userMessage = new ChatMessageModel(actionMessage, MessageAuthor.User); + var userMessage = new ChatMessageModel(actionText, MessageAuthor.User); Messages.Add(userMessage); - _logger.LogInformation("Kullanıcı hızlı eylem seçti: {ActionText}", actionText); + _logger.LogInformation("User selected a quick action: {ActionText}", actionText); - await ProcessBotResponseAsync(actionMessage); + await GetBotResponseAsync(botCommandMessage); } - private async Task ProcessBotResponseAsync(string userMessage) + private async Task GetBotResponseAsync(string messageForBot) { - await Task.Delay(1000); // Bot "yazıyor..." efekti için küçük bir bekleme - - ChatMessageModel botResponse; - string lowerUserMessage = userMessage.ToLower(); + IsAwaitingBotResponse = true; + SendMessageCommand.NotifyCanExecuteChanged(); - botResponse = new ChatMessageModel(lowerUserMessage, MessageAuthor.Bot); + try + { + var chatResponse = await _apiService.PostAsync("Chat/send", new ChatRequestDto + { + Message = messageForBot, + ClientChatSessionId = _clientChatSessionId, + }); - Messages.Add(botResponse); + if (chatResponse != null) + { + var botResponse = new ChatMessageModel(chatResponse.Reply ?? "Sorry, I couldn't process that.", MessageAuthor.Bot); + Messages.Add(botResponse); + } + else + { + var errorResponse = new ChatMessageModel("I seem to be having trouble connecting. Please try again later.", MessageAuthor.Bot); + Messages.Add(errorResponse); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting response from Chat API."); + var errorResponse = new ChatMessageModel("An error occurred. I can't respond right now.", MessageAuthor.Bot); + Messages.Add(errorResponse); + } + finally + { + IsAwaitingBotResponse = false; + SendMessageCommand.NotifyCanExecuteChanged(); + } } } -} +} \ No newline at end of file diff --git a/FinTrack/ViewModels/ForgotPasswordViewModel.cs b/FinTrack/ViewModels/ForgotPasswordViewModel.cs index 0ecba24..f99ca59 100644 --- a/FinTrack/ViewModels/ForgotPasswordViewModel.cs +++ b/FinTrack/ViewModels/ForgotPasswordViewModel.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using System.Windows; +using FinTrackForWindows.Services.AppInNotifications; +using Microsoft.Extensions.Logging; namespace FinTrackForWindows.ViewModels { @@ -11,17 +12,30 @@ public partial class ForgotPasswordViewModel : ObservableObject public event Action? NavigateToLoginRequested; + private readonly ILogger _logger; + private readonly IAppInNotificationService _appInNotificationService; + + public ForgotPasswordViewModel(ILogger logger, IAppInNotificationService appInNotificationService) + { + _logger = logger; + _appInNotificationService = appInNotificationService; + + _logger.LogInformation("ForgotPasswordViewModel initialized."); + } + [RelayCommand] private void ResetPassword_ForgotPasswordView_Button() { if (!string.IsNullOrEmpty(Email_ForgotPasswordView_TextBox)) { - MessageBox.Show($"Şifre sıfırlama bağlantısı {Email_ForgotPasswordView_TextBox} adresine gönderildi."); + _appInNotificationService.ShowInfo($"The password reset link has been sent to {Email_ForgotPasswordView_TextBox}."); + _logger.LogInformation($"Password reset link sent to {Email_ForgotPasswordView_TextBox}."); } else { - MessageBox.Show("Lütfen geçerli bir e-posta adresi girin."); + _appInNotificationService.ShowError("Please enter a valid email address."); + _logger.LogWarning("Password reset attempted with an empty email field."); } } diff --git a/FinTrack/ViewModels/LoginViewModel.cs b/FinTrack/ViewModels/LoginViewModel.cs index 1da99aa..fb9c8f5 100644 --- a/FinTrack/ViewModels/LoginViewModel.cs +++ b/FinTrack/ViewModels/LoginViewModel.cs @@ -4,8 +4,8 @@ using FinTrackForWindows.Core; using FinTrackForWindows.Messages; using FinTrackForWindows.Services; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -31,39 +31,45 @@ public partial class LoginViewModel : ObservableObject private readonly ILogger _logger; + private readonly IAppInNotificationService _appInNotificationService; + public LoginViewModel( IAuthService authService, ILogger logger, - ISecureTokenStorage secureTokenStorage) + ISecureTokenStorage secureTokenStorage, + IAppInNotificationService appInNotificationService) { _authService = authService; _logger = logger; _secureTokenStorage = secureTokenStorage; + _appInNotificationService = appInNotificationService; } [RelayCommand] private async Task Login_LoginView_Button() { - _logger.LogInformation("Kullanıcı giriş yapmaya çalışıyor. E-posta: {Email}", Email_LoginView_TextBox); + _logger.LogInformation("User attempting to log in. Email: {Email}", Email_LoginView_TextBox); if (string.IsNullOrEmpty(Email_LoginView_TextBox) || string.IsNullOrEmpty(Password_LoginView_TextBox)) { - MessageBox.Show("Lütfen e-posta ve şifre alanlarını doldurun.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Kullanıcı giriş yapmaya çalıştı ancak e-posta veya şifre alanları boş."); + _appInNotificationService.ShowInfo("Please enter both your email and password."); + _logger.LogWarning("Login attempt failed: Email or password field is empty."); return; } string token = await _authService.LoginAsync(Email_LoginView_TextBox, Password_LoginView_TextBox); if (string.IsNullOrEmpty(token)) { - MessageBox.Show("Giriş başarısız oldu. Lütfen e-posta ve şifrenizi kontrol edin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogError("Kullanıcı giriş yapmaya çalıştı ancak token alınamadı. E-posta veya şifre hatalı olabilir."); + _appInNotificationService.ShowError("Login failed. Please check your email and password and try again."); + _logger.LogError("Login attempt failed: Unable to retrieve token. Invalid email or password."); return; } SessionManager.SetToken(token); _secureTokenStorage.SaveToken(token); - _logger.LogInformation("Kullanıcı giriş yaptı ve token kaydedildi."); + _logger.LogInformation("User logged in successfully. Token has been saved."); + + _appInNotificationService.ShowSuccess("Login successful. Redirecting to the main page."); WeakReferenceMessenger.Default.Send(new LoginSuccessMessage()); } @@ -76,21 +82,21 @@ private void TogglePasswordVisibility_LoginView_Button() EyeIconSource_LoginView_Image = IsPasswordVisible_LoginView_PasswordBoxAndTextBox ? "/Assets/Images/Icons/eyeopen.png" : "/Assets/Images/Icons/eyeclose.png"; - _logger.LogInformation("Şifre görünürlüğü değiştirildi. Şifre {0}.", IsPasswordVisible_LoginView_PasswordBoxAndTextBox ? "görünür" : "gizli"); + _logger.LogInformation("Password visibility toggled. Password is now {0}.", IsPasswordVisible_LoginView_PasswordBoxAndTextBox ? "visible" : "hidden"); } [RelayCommand] private void NavigateToRegister_LoginView_Button() { NavigateToRegisterRequested?.Invoke(); - _logger.LogInformation("Kullanıcı kayıt sayfasına yönlendirildi."); + _logger.LogInformation("Navigating to the registration page."); } [RelayCommand] private void NavigateToForgotPassword_LoginView_Button() { NavigateToForgotPasswordRequested?.Invoke(); - _logger.LogInformation("Kullanıcı şifremi unuttum sayfasına yönlendirildi."); + _logger.LogInformation("Navigating to the forgot password page."); } } } \ No newline at end of file diff --git a/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs b/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs index e8e32d5..7cd5b53 100644 --- a/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs +++ b/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs @@ -4,9 +4,9 @@ using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Settings; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -26,11 +26,13 @@ public partial class NotificationSettingsContentViewModel : ObservableObject private readonly ILogger _logger; private readonly IApiService _apiService; + private readonly IAppInNotificationService _appInNotificationService; - public NotificationSettingsContentViewModel(ILogger logger, IApiService apiService) + public NotificationSettingsContentViewModel(ILogger logger, IApiService apiService, IAppInNotificationService appInNotificationService) { _logger = logger; _apiService = apiService; + _appInNotificationService = appInNotificationService; EmailNotificationSettings = new ObservableCollection(); _ = LoadSettings(); @@ -62,7 +64,7 @@ private async Task LoadSettings() catch (Exception ex) { _logger.LogError(ex, "Failed to load notification settings."); - MessageBox.Show("Could not load notification settings. Default values will be used.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); + _appInNotificationService.ShowError("Failed to load notification settings. Default values will be used.", ex); InitializeDefaultSettings(); } finally @@ -89,12 +91,12 @@ private async Task SaveChanges() await _apiService.PostAsync("UserSettings/UserNotificationSettings", settingsUpdateDto); _logger.LogInformation("User notification settings have been successfully saved."); - MessageBox.Show("Your notification settings have been saved.", "Notification Settings", MessageBoxButton.OK, MessageBoxImage.Information); + _appInNotificationService.ShowInfo("Your notification settings have been saved successfully."); } catch (Exception ex) { _logger.LogError(ex, "Failed to save notification settings."); - MessageBox.Show("An error occurred while saving your settings. Please try again.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("Failed to save notification settings.", ex); } finally { diff --git a/FinTrack/ViewModels/NotificationViewModel.cs b/FinTrack/ViewModels/NotificationViewModel.cs index 696a8c9..177aab2 100644 --- a/FinTrack/ViewModels/NotificationViewModel.cs +++ b/FinTrack/ViewModels/NotificationViewModel.cs @@ -3,9 +3,9 @@ using FinTrackForWindows.Dtos.NotificationDtos; using FinTrackForWindows.Models.Notification; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -26,11 +26,13 @@ public partial class NotificationViewModel : ObservableObject private readonly ILogger _logger; private readonly IApiService _apiService; + private readonly IAppInNotificationService _appInNotificationService; - public NotificationViewModel(ILogger logger, IApiService apiService) + public NotificationViewModel(ILogger logger, IApiService apiService, IAppInNotificationService appInNotificationService) { _logger = logger; _apiService = apiService; + _appInNotificationService = appInNotificationService; _ = LoadNotifications(); } @@ -59,7 +61,7 @@ private async Task LoadNotifications() catch (Exception ex) { _logger.LogError(ex, "Failed to load notifications."); - MessageBox.Show("Could not load notifications. Please check your connection and try again.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("Could not load notifications. Please check your connection and try again.", ex); Notifications.Clear(); } finally @@ -89,7 +91,7 @@ private async Task MarkAllAsRead() catch (Exception ex) { _logger.LogError(ex, "Failed to mark all notifications as read."); - MessageBox.Show("An error occurred. Could not mark all notifications as read.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while marking notifications as read.", ex); } } @@ -107,7 +109,7 @@ private async Task ClearAll() catch (Exception ex) { _logger.LogError(ex, "Failed to clear all notifications."); - MessageBox.Show("An error occurred. Could not clear all notifications.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while clearing notifications.", ex); } } @@ -125,7 +127,7 @@ private async Task MarkAsRead(NotificationModel? notification) catch (Exception ex) { _logger.LogError(ex, "Failed to mark notification {NotificationId} as read.", notification.Id); - MessageBox.Show("An error occurred. The notification could not be marked as read.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while marking the notification as read.", ex); } } @@ -143,7 +145,7 @@ private async Task DeleteNotification(NotificationModel? notification) catch (Exception ex) { _logger.LogError(ex, "Failed to delete notification {NotificationId}.", notification.Id); - MessageBox.Show("An error occurred. The notification could not be deleted.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _appInNotificationService.ShowError("An error occurred while deleting the notification.", ex); } } } diff --git a/FinTrack/ViewModels/OtpVerificationViewModel.cs b/FinTrack/ViewModels/OtpVerificationViewModel.cs index 91bd108..a8a5864 100644 --- a/FinTrack/ViewModels/OtpVerificationViewModel.cs +++ b/FinTrack/ViewModels/OtpVerificationViewModel.cs @@ -2,8 +2,8 @@ using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Core; using FinTrackForWindows.Services; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -24,11 +24,13 @@ public partial class OtpVerificationViewModel : ObservableObject private readonly IAuthService _authService; private readonly ILogger _logger; + private readonly IAppInNotificationService _appInNotificationService; - public OtpVerificationViewModel(IAuthService authService, ILogger logger) + public OtpVerificationViewModel(IAuthService authService, ILogger logger, IAppInNotificationService appInNotificationService) { _authService = authService; _logger = logger; + _appInNotificationService = appInNotificationService; } public void StartCounter() @@ -52,24 +54,24 @@ private async Task VerifyOtpCode_OtpVerificationView_Button() { if (string.IsNullOrWhiteSpace(VerificationCode_OtpVerificationView_TextBox) || VerificationCode_OtpVerificationView_TextBox.Length != 6) { - MessageBox.Show("Lütfen geçerli bir OTP kodu girin (6 haneli).", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Geçersiz OTP kodu girişi. Kullanıcıdan tekrar denemesi istendi."); + _appInNotificationService.ShowError("Please enter a valid 6-digit OTP code."); + _logger.LogWarning("Invalid OTP code entry. Prompted user to try again."); return; } bool isVerify = await _authService.VerifyOtpAndRegisterCodeAsync(NewUserInformationManager.Email ?? null!, VerificationCode_OtpVerificationView_TextBox ?? null!); if (!isVerify) { - MessageBox.Show("OTP doğrulama başarısız oldu. Lütfen kodu kontrol edin ve tekrar deneyin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogError("OTP doğrulama başarısız. E-posta: {Email}, Kod: {Code}", NewUserInformationManager.Email, VerificationCode_OtpVerificationView_TextBox); + _appInNotificationService.ShowError("OTP verification failed. Please check the code and try again."); + _logger.LogError("OTP verification failed. Email: {Email}, Code: {Code}", NewUserInformationManager.Email, VerificationCode_OtpVerificationView_TextBox); return; } - MessageBox.Show("OTP doğrulama başarılı! Hoş geldiniz!", "Bilgi", MessageBoxButton.OK, MessageBoxImage.Information); - _logger.LogInformation("OTP doğrulama başarılı. Kullanıcı kaydı tamamlandı. E-posta: {Email}", NewUserInformationManager.Email); + _appInNotificationService.ShowSuccess("OTP verification successful! Welcome!"); + _logger.LogInformation("OTP verification successful. User registration completed. Email: {Email}", NewUserInformationManager.Email); - NewUserInformationManager.FullName = null; // Clear the stored user information - NewUserInformationManager.Email = null; // Clear the stored user information - NewUserInformationManager.Password = null; // Clear the stored user information + NewUserInformationManager.FullName = null; + NewUserInformationManager.Email = null; + NewUserInformationManager.Password = null; NavigateToLoginRequested?.Invoke(); } @@ -83,24 +85,24 @@ private async Task CodeNotFound_OtpVerificationView_Button() string.IsNullOrEmpty(NewUserInformationManager.Password)) { bool isInitiateRegistration = await _authService.InitiateRegistrationAsnc( - NewUserInformationManager.FirstName, - NewUserInformationManager.LastName, - NewUserInformationManager.Email, - NewUserInformationManager.Password); + NewUserInformationManager.FirstName ?? string.Empty, + NewUserInformationManager.LastName ?? string.Empty, + NewUserInformationManager.Email ?? string.Empty, + NewUserInformationManager.Password ?? string.Empty); if (!isInitiateRegistration) { - MessageBox.Show("Kayıt işlemi başarısız oldu. Lütfen daha sonra tekrar deneyin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogError("Kayıt işlemi başarısız. E-posta: {Email}", NewUserInformationManager.Email); + _appInNotificationService.ShowError("Registration failed. Please try again later."); + _logger.LogError("Registration failed. Email: {Email}", NewUserInformationManager.Email); return; } - MessageBox.Show("Doğrulama kodu tekrar gönderildi.", "Bilgi", MessageBoxButton.OK, MessageBoxImage.Information); - _logger.LogInformation("Doğrulama kodu tekrar gönderildi. E-posta: {Email}", NewUserInformationManager.Email); + _appInNotificationService.ShowInfo("Verification code has been resent. Please check your email."); + _logger.LogInformation("Verification code resent. Email: {Email}", NewUserInformationManager.Email); } else { - MessageBox.Show("Kod gönderirken bir sorun oluştu. Bilgileri doğru giriniz.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Kod gönderme sırasında eksik kullanıcı bilgileri. Kullanıcı kayıt sayfasına yönlendiriliyor."); + _appInNotificationService.ShowError("An issue occurred while sending the code. Please check your information."); + _logger.LogWarning("Missing user information during code sending. Redirecting user to registration page."); NavigateToRegisterRequested?.Invoke(); } } @@ -109,7 +111,7 @@ private async Task CodeNotFound_OtpVerificationView_Button() private void NavigateToRegister_OtpVerificationView_Button() { NavigateToRegisterRequested?.Invoke(); - _logger.LogInformation("Kullanıcı giriş sayfasına yönlendirildi."); + _logger.LogInformation("User redirected to the registration page."); } } } diff --git a/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs b/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs index 3624a97..f4ecf28 100644 --- a/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs +++ b/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs @@ -2,8 +2,8 @@ using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Dtos.SettingsDtos; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -22,10 +22,14 @@ public partial class ProfileSettingsContentViewModel : ObservableObject private readonly IApiService _apiService; - public ProfileSettingsContentViewModel(ILogger logger, IApiService apiService) + private readonly IAppInNotificationService _appInNotificationService; + + public ProfileSettingsContentViewModel(ILogger logger, IApiService apiService, IAppInNotificationService appInNotificationService) { _logger = logger; _apiService = apiService; + _appInNotificationService = appInNotificationService; + _ = LoadProfileData(); } @@ -49,8 +53,8 @@ private async Task ProfileSettingsContentSaveChanges() Email = Email, ProfilePictureUrl = ProfilePhotoUrl }); - _logger.LogInformation("Yeni profil bilgileri kaydedildi: {FullName}, {Email}, {ProfilePhotoUrl}", FullName, Email, ProfilePhotoUrl); - MessageBox.Show("Profil bilgileri başarıyla kaydedildi.", "Bilgi", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation("New profile information saved: {FullName}, {Email}, {ProfilePhotoUrl}", FullName, Email, ProfilePhotoUrl); + _appInNotificationService.ShowSuccess("Your profile information has been successfully saved."); } } } diff --git a/FinTrack/ViewModels/RegisterViewModel.cs b/FinTrack/ViewModels/RegisterViewModel.cs index ed01f8d..b6e4ab3 100644 --- a/FinTrack/ViewModels/RegisterViewModel.cs +++ b/FinTrack/ViewModels/RegisterViewModel.cs @@ -2,9 +2,9 @@ using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Core; using FinTrackForWindows.Services; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; using System.Text.RegularExpressions; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -33,13 +33,17 @@ public partial class RegisterViewModel : ObservableObject private readonly ILogger _logger; private readonly IAuthService _authService; + private readonly IAppInNotificationService _appInNotificationService; public event EventHandler SendOtpVerificationRequested; - public RegisterViewModel(ILogger logger, IAuthService authService) + public RegisterViewModel(ILogger logger, + IAuthService authService, + IAppInNotificationService appInNotificationService) { _logger = logger; _authService = authService; + _appInNotificationService = appInNotificationService; } [RelayCommand] @@ -51,23 +55,23 @@ private async Task Register_RegisterView_Button() string.IsNullOrEmpty(Email_RegisterView_TextBox) || string.IsNullOrEmpty(Password_RegisterView_TextBox)) { - MessageBox.Show("Lütfen tüm alanları doldurun.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Kayıt işlemi için gerekli alanlar boş bırakıldı."); + _appInNotificationService.ShowWarning("Please fill in all required fields."); + _logger.LogWarning("Registration attempt failed: Required fields are missing."); return; } bool isValidEmail = Email_RegisterView_TextBox.Contains("@") && Email_RegisterView_TextBox.Contains("."); if (!isValidEmail) { - MessageBox.Show("Lütfen geçerli bir e-posta adresi girin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Kayıt işlemi için geçersiz e-posta adresi girildi: {Email}", Email_RegisterView_TextBox); + _appInNotificationService.ShowError("Please enter a valid email address."); + _logger.LogWarning("Registration attempt failed: Invalid email address provided: {Email}", Email_RegisterView_TextBox); return; } - if (IsPasswordValid(Password_RegisterView_TextBox) == false) + if (!IsPasswordValid(Password_RegisterView_TextBox)) { - MessageBox.Show("Şifre en az 8 karakter uzunluğunda, en az bir büyük harf, bir küçük harf, bir rakam ve bir özel karakter içermelidir.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogWarning("Kayıt işlemi için geçersiz şifre girildi."); + _appInNotificationService.ShowError("Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one digit, and one special character."); + _logger.LogWarning("Registration attempt failed: Password does not meet complexity requirements."); return; } @@ -78,15 +82,14 @@ private async Task Register_RegisterView_Button() Password_RegisterView_TextBox); if (!isInitiateRegistration) { - MessageBox.Show("Kayıt işlemi başarısız oldu. Lütfen daha sonra tekrar deneyin.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); - _logger.LogError("Kayıt işlemi başarısız oldu. E-posta: {Email}", Email_RegisterView_TextBox); + _appInNotificationService.ShowError("Registration failed. Please try again later."); + _logger.LogError("Registration process failed for email: {Email}", Email_RegisterView_TextBox); return; } SendOtpVerificationRequested?.Invoke(this, EventArgs.Empty); - MessageBox.Show("Kayıt işlemi başarılı! Lütfen e-posta adresinize gelen doğrulama kodunu girin.", "Bilgi", MessageBoxButton.OK, MessageBoxImage.Information); - _logger.LogInformation("Kayıt işlemi başarılı. E-posta: {Email}", Email_RegisterView_TextBox); + _appInNotificationService.ShowSuccess("Registration successful! Please enter the verification code sent to your email address."); + _logger.LogInformation("Registration successful for email: {Email}", Email_RegisterView_TextBox); - // Store user information in the static manager NewUserInformationManager.FirstName = FirstName_RegisterView_TextBox; NewUserInformationManager.LastName = LastName_RegisterView_TextBox; NewUserInformationManager.FullName = FirstName_RegisterView_TextBox.Replace(" ", "").Trim() + "_" + LastName_RegisterView_TextBox.Replace(" ", "").Trim(); @@ -111,14 +114,14 @@ private void TogglePasswordVisibility_RegisterView_Button() EyeIconSource_RegisterView_Image = IsPasswordVisible_RegisterView_PasswordBoxAndTextBox ? "/Assets/Images/Icons/eyeopen.png" : "/Assets/Images/Icons/eyeclose.png"; - _logger.LogInformation("Şifre görünürlüğü değiştirildi. Yeni durum: {IsVisible}", IsPasswordVisible_RegisterView_PasswordBoxAndTextBox); + _logger.LogInformation("Password visibility toggled. New state: {IsVisible}", IsPasswordVisible_RegisterView_PasswordBoxAndTextBox); } [RelayCommand] private void NavigateToLogin_RegisterView_Button() { NavigateToLoginRequested?.Invoke(); - _logger.LogInformation("Kullanıcı kayıt ekranından giriş ekranına yönlendirildi."); + _logger.LogInformation("User navigated from registration to login view."); } } } \ No newline at end of file diff --git a/FinTrack/ViewModels/ReportsViewModel.cs b/FinTrack/ViewModels/ReportsViewModel.cs index 87a0fd1..27120a1 100644 --- a/FinTrack/ViewModels/ReportsViewModel.cs +++ b/FinTrack/ViewModels/ReportsViewModel.cs @@ -1,25 +1,22 @@ -// ViewModels/ReportsViewModel.cs -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using FinTrackForWindows.Dtos.AccountDtos; -using FinTrackForWindows.Dtos.CategoryDtos; using FinTrackForWindows.Dtos.ReportDtos; using FinTrackForWindows.Enums; using FinTrackForWindows.Helpers; using FinTrackForWindows.Models.Report; -using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.AppInNotifications; +using FinTrackForWindows.Services.Reports; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Text; -using System.Windows; namespace FinTrackForWindows.ViewModels { - // partial class olduğundan emin olun public partial class ReportsViewModel : ObservableObject { private readonly ILogger _logger; - private readonly IApiService _apiService; + private readonly IReportStore _reportStore; + private readonly IAppInNotificationService _notificationService; public ObservableCollection AvailableReportTypes { get; } public ObservableCollection AvailableAccounts { get; } @@ -27,7 +24,6 @@ public partial class ReportsViewModel : ObservableObject public ObservableCollection SortingCriteria { get; } public ObservableCollection AvailableDocumentFormats { get; } - // --- DOĞRU KULLANIM: Sadece [ObservableProperty] ile private alanları tanımlayın --- [ObservableProperty] [NotifyPropertyChangedFor(nameof(ReportSummary))] private ReportType selectedReportType; @@ -53,7 +49,7 @@ public partial class ReportsViewModel : ObservableObject private bool isExpenseSelected = true; [ObservableProperty] - private string selectedSortingCriterion; + private string? selectedSortingCriterion; [ObservableProperty] private DocumentFormat selectedDocumentFormat; @@ -73,16 +69,17 @@ public string ReportSummary } } - public ReportsViewModel(ILogger logger, IApiService apiService) + public ReportsViewModel(ILogger logger, IReportStore reportStore, IAppInNotificationService appInNotificationService) { _logger = logger; - _apiService = apiService; + _reportStore = reportStore; + _notificationService = appInNotificationService; - AvailableReportTypes = new ObservableCollection(Enum.GetValues(typeof(ReportType)).Cast()); - AvailableAccounts = new ObservableCollection(); - AvailableCategories = new ObservableCollection(); - SortingCriteria = new ObservableCollection(); - AvailableDocumentFormats = new ObservableCollection(Enum.GetValues(typeof(DocumentFormat)).Cast()); + AvailableReportTypes = _reportStore.AvailableReportTypes; + AvailableAccounts = _reportStore.AvailableAccounts; + AvailableCategories = _reportStore.AvailableCategories; + SortingCriteria = _reportStore.SortingCriteria; + AvailableDocumentFormats = _reportStore.AvailableDocumentFormats; _ = LoadInitialDataAsync(); } @@ -92,42 +89,16 @@ private async Task LoadInitialDataAsync() IsBusy = true; try { - var accountsFromApi = await _apiService.GetAsync>("Account"); - var categoriesFromApi = await _apiService.GetAsync>("categories"); - - AvailableAccounts.Clear(); - // "All" seçeneğini artık ViewModel'de kontrol edeceğimiz için eklemeye gerek yok. - if (accountsFromApi != null) - { - foreach (var acc in accountsFromApi) - { - AvailableAccounts.Add(new SelectableOptionReport(acc.Id, acc.Name)); - } - } - - AvailableCategories.Clear(); - if (categoriesFromApi != null) - { - foreach (var cat in categoriesFromApi) - { - AvailableCategories.Add(new SelectableOptionReport(cat.Id, cat.Name)); - } - } - - SortingCriteria.Clear(); - SortingCriteria.Add("By Date (Newest to Oldest)"); - SortingCriteria.Add("By Date (Oldest to Newest)"); - SortingCriteria.Add("By Amount (Highest to Lowest)"); - SortingCriteria.Add("By Amount (Lowest to Highest)"); + await _reportStore.LoadInitialDataAsync(); SelectedReportType = AvailableReportTypes.FirstOrDefault(); - SelectedSortingCriterion = SortingCriteria.FirstOrDefault(); + SelectedSortingCriterion = SortingCriteria?.FirstOrDefault(); SelectedDocumentFormat = AvailableDocumentFormats.FirstOrDefault(); } catch (Exception ex) { - _logger.LogError(ex, "Failed to load initial data for reports."); - MessageBox.Show("Could not load account and category data. Please check your internet connection.", "Connection Error"); + _logger.LogError(ex, "Failed to load initial data for reports from the Store."); + _notificationService.ShowError("Could not load report options. Please check your internet connection."); } finally { @@ -170,28 +141,25 @@ private async Task CreateReport() .ToList() }; - _logger.LogInformation("Sending report creation request. Type: {ReportType}", reportRequest.ReportType); + _logger.LogInformation("Delegating report creation to ReportStore."); - var result = await _apiService.PostAndDownloadReportAsync("Reports/generate", reportRequest); + string? savedPath = await _reportStore.CreateAndSaveReportAsync(reportRequest); - if (result.HasValue && result.Value.FileBytes.Length > 0) + if (!string.IsNullOrEmpty(savedPath)) { - var (fileBytes, fileName) = result.Value; - string savedPath = await FileSaver.SaveReportToDocumentsAsync(fileBytes, fileName); - _logger.LogInformation("Report saved successfully: {Path}", savedPath); - MessageBox.Show($"Report created successfully and saved to '{savedPath}'.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + _notificationService.ShowSuccess($"Report created successfully and saved to '{savedPath}'."); FileSaver.OpenContainingFolder(savedPath); } else { - _logger.LogWarning("No file data received from API or no data found for the report."); - MessageBox.Show("Could not create report. No data found for the specified criteria or a server error occurred.", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning); + _logger.LogWarning("ReportStore reported failure in report creation."); + _notificationService.ShowWarning("Could not create report. No data found for the specified criteria."); } } catch (Exception ex) { - _logger.LogError(ex, "An error occurred while creating the report."); - MessageBox.Show($"An unexpected error occurred:\n\n{ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.LogError(ex, "An unexpected error occurred in ViewModel while creating the report."); + _notificationService.ShowError($"An unexpected error occurred: {ex.Message}"); } finally { diff --git a/FinTrack/ViewModels/SecuritySettingsContentViewModel.cs b/FinTrack/ViewModels/SecuritySettingsContentViewModel.cs index a0a9847..846c0bb 100644 --- a/FinTrack/ViewModels/SecuritySettingsContentViewModel.cs +++ b/FinTrack/ViewModels/SecuritySettingsContentViewModel.cs @@ -1,7 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Services.AppInNotifications; using Microsoft.Extensions.Logging; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -15,16 +15,19 @@ public partial class SecuritySettingsContentViewModel : ObservableObject readonly ILogger _logger; - public SecuritySettingsContentViewModel(ILogger logger) + private readonly IAppInNotificationService _appInNotificationService; + + public SecuritySettingsContentViewModel(ILogger logger, IAppInNotificationService appInNotificationService) { _logger = logger; + _appInNotificationService = appInNotificationService; } [RelayCommand] private void SecuritySettingsContentSaveChanges() { - _logger.LogInformation("Güvenlik ayarları kaydedildi."); - MessageBox.Show("Güvenlik ayarları kaydedildi.", "Ayarlar", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation("Security settings saved."); + _appInNotificationService.ShowSuccess("Security settings saved."); } } } diff --git a/FinTrack/ViewModels/SettingsViewModel.cs b/FinTrack/ViewModels/SettingsViewModel.cs index 0f7f455..4de1a4f 100644 --- a/FinTrack/ViewModels/SettingsViewModel.cs +++ b/FinTrack/ViewModels/SettingsViewModel.cs @@ -1,10 +1,10 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Dtos.MembershipDtos; +using FinTrackForWindows.Services.AppInNotifications; using FinTrackForWindows.Services.Memberships; using Microsoft.Extensions.Logging; using System.Diagnostics; -using System.Windows; namespace FinTrackForWindows.ViewModels { @@ -23,13 +23,16 @@ public partial class SettingsViewModel : ObservableObject public IMembershipStore MembershipStore => _membershipStore; + private readonly IAppInNotificationService _appInNotificationService; + public SettingsViewModel( ILogger logger, IMembershipStore membershipStore, ProfileSettingsContentViewModel profileVM, SecuritySettingsContentViewModel securityVM, NotificationSettingsContentViewModel notificationsVM, - AppSettingsContentViewModel appVM) + AppSettingsContentViewModel appVM, + IAppInNotificationService appInNotificationService) { _logger = logger; _membershipStore = membershipStore; @@ -37,6 +40,7 @@ public SettingsViewModel( _securityVM = securityVM; _notificationsVM = notificationsVM; _appVM = appVM; + _appInNotificationService = appInNotificationService; _currentPageViewModel = _profileVM; @@ -75,7 +79,8 @@ private async Task SelectPlan(PlanFeatureDto selectedPlan) if (MembershipStore.CurrentUserMembership != null && MembershipStore.CurrentUserMembership.PlanId == selectedPlan.Id) { - MessageBox.Show("This is already your current plan.", "Information", MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInformation($"User is already subscribed to the selected plan: {selectedPlan.Name}"); + _appInNotificationService.ShowInfo($"You are already subscribed to the {selectedPlan.Name} plan."); return; } @@ -91,13 +96,13 @@ private async Task SelectPlan(PlanFeatureDto selectedPlan) 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); + _appInNotificationService.ShowError($"Could not open the payment page. Please copy this link and open it manually:\n\n{checkoutUrl}"); } } 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); + _appInNotificationService.ShowSuccess("Your subscription status has been updated. Please check your profile."); } } } diff --git a/FinTrack/ViewModels/TopBarViewModel.cs b/FinTrack/ViewModels/TopBarViewModel.cs index 7d74ad6..c68dd78 100644 --- a/FinTrack/ViewModels/TopBarViewModel.cs +++ b/FinTrack/ViewModels/TopBarViewModel.cs @@ -1,8 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Core; -using FinTrackForWindows.Dtos; using FinTrackForWindows.Services.Api; +using FinTrackForWindows.Services.Users; using Microsoft.Extensions.Logging; namespace FinTrackForWindows.ViewModels @@ -36,14 +36,17 @@ public partial class TopBarViewModel : ObservableObject private readonly ILogger _logger; private readonly IApiService _apiService; - public TopBarViewModel(ILogger logger, IApiService apiService) + private readonly IUserStore _userStore; + + public TopBarViewModel(ILogger logger, IApiService apiService, IUserStore userStore) { _logger = logger; _apiService = apiService; + _userStore = userStore; if (SessionManager.IsLoggedIn) { - _logger.LogInformation("Kullanıcı zaten giriş yapmış. TopBarViewModel profil bilgilerini yüklüyor."); + _logger.LogInformation("User is already logged in. Loading profile information in TopBarViewModel."); _ = LoadProfile(); } } @@ -52,23 +55,26 @@ private async Task LoadProfile() { if (!SessionManager.IsLoggedIn) { - _logger.LogWarning("Kullanıcı oturumu açık değil, profil bilgileri yüklenemedi."); + _logger.LogWarning("User session is not active. Unable to load profile information."); return; } - var userProfile = await _apiService.GetAsync("User"); + + await _userStore.LoadCurrentUserAsync(); + + var userProfile = _userStore.CurrentUser; if (userProfile != null) { - UserAvatar = userProfile.ProfilePicture; + UserAvatar = userProfile.ProfilePictureUrl; UserFullName = userProfile.UserName; UserEmail = userProfile.Email; - UserMembershipType = userProfile.MembershipType; - _logger.LogInformation("Kullanıcı profili başarıyla yüklendi. Kullanıcı Adı: {UserName}, Email: {Email}, Üyelik Tipi: {MembershipType}", - userProfile.UserName, userProfile.Email, userProfile.MembershipType); + UserMembershipType = $"{userProfile.MembershipPlan} Member"; + _logger.LogInformation("User profile loaded successfully. Name: {UserName}, Email: {Email}, Membership Type: {MembershipType}", + userProfile.UserName, userProfile.Email, userProfile.MembershipPlan); } else { - _logger.LogWarning("Kullanıcı profili yüklenemedi."); + _logger.LogWarning("Failed to load user profile."); } } @@ -76,77 +82,77 @@ private async Task LoadProfile() private void NavigateToDashboard_TopBarView_Button() { NavigateToDashboardRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Dashboard paneline geçti."); + _logger.LogInformation("Navigated to Dashboard panel."); } [RelayCommand] private void NavigateToAccount_TopBarView_Button() { NavigateToAccountRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Account paneline geçti."); + _logger.LogInformation("Navigated to Account panel."); } [RelayCommand] private void NavigateToBudget_TopBarView_Button() { NavigateToBudegtRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Budget paneline geçti."); + _logger.LogInformation("Navigated to Budget panel."); } [RelayCommand] private void NavigateToTransactions_TopBarView_Button() { NavigateToTransactionsRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Transactions paneline geçti."); + _logger.LogInformation("Navigated to Transactions panel."); } [RelayCommand] private void NavigateToCurrencies_TopBarView_Button() { NavigateToCurrenciesRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Currencies paneline geçti."); + _logger.LogInformation("Navigated to Currencies panel."); } [RelayCommand] private void NavigateToDebt_TopBarView_Button() { NavigateToDebtRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Debt paneline geçti."); + _logger.LogInformation("Navigated to Debt panel."); } [RelayCommand] private void NavigateToReports_TopBarView_Button() { NavigateToReportsRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Reports paneline geçti."); + _logger.LogInformation("Navigated to Reports panel."); } [RelayCommand] private void NavigateToFinBot_TopBarView_Button() { NavigateToFinBotRequested?.Invoke(); - _logger.LogInformation("Kullanıcı FinBot paneline geçti."); + _logger.LogInformation("Navigated to FinBot panel."); } [RelayCommand] private void NavigateToFeedback_TopBarView_Button() { NavigateToFeedbackRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Feedback paneline geçti."); + _logger.LogInformation("Navigated to Feedback panel."); } [RelayCommand] private void NavigateToNotification_TopBarView_Button() { NavigateToNotificationRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Notification paneline geçti."); + _logger.LogInformation("Navigated to Notification panel."); } [RelayCommand] private void NavigateToSettings_TopBarView_Button() { NavigateToSettingsRequested?.Invoke(); - _logger.LogInformation("Kullanıcı Settings paneline geçti."); + _logger.LogInformation("Navigated to Settings panel."); } } } diff --git a/FinTrack/ViewModels/TransactionsViewModel.cs b/FinTrack/ViewModels/TransactionsViewModel.cs index 88b8366..d76bc89 100644 --- a/FinTrack/ViewModels/TransactionsViewModel.cs +++ b/FinTrack/ViewModels/TransactionsViewModel.cs @@ -27,19 +27,22 @@ public partial class TransactionsViewModel : ObservableObject private readonly ITransactionStore _transactionStore; private readonly IAccountStore _accountStore; - // Renk paleti - private static readonly SKColor PrimaryColor = new SKColor(100, 181, 246); // Mavi - private static readonly SKColor SecondaryColor = new SKColor(239, 83, 80); // Kırmızı + private static readonly SKColor PrimaryColor = new SKColor(100, 181, 246); + private static readonly SKColor SecondaryColor = new SKColor(239, 83, 80); private static readonly SKColor[] PieChartColors = new[] { new SKColor(100, 181, 246), // Mavi - new SKColor(239, 83, 80), // Kırmızı + new SKColor(239, 83, 80), // Kırmızı new SKColor(255, 167, 38), // Turuncu new SKColor(129, 199, 132), // Yeşil - new SKColor(171, 71, 188) // Mor + new SKColor(171, 71, 188), // Mor + new SKColor(255, 193, 7), // Sarı + new SKColor(96, 125, 139), // Gri-Mavi + new SKColor(233, 30, 99), // Pembe + new SKColor(156, 39, 176), // Menekşe + new SKColor(63, 81, 181) // İndigo }; - // Koleksiyonlar public ReadOnlyObservableCollection Transactions => _transactionStore.Transactions; public ObservableCollection FilteredTransactions { get; } public ReadOnlyObservableCollection AllAccounts => _accountStore.Accounts; @@ -48,31 +51,38 @@ public partial class TransactionsViewModel : ObservableObject private ObservableCollection allCategories; public ObservableCollection CategoriesForFilter { get; } - // Filtreleme Özellikleri [ObservableProperty] private string? filterByDescription; + [ObservableProperty] private string? filterByTransactionType; + [ObservableProperty] private DateTime? filterByStartDate; + [ObservableProperty] private DateTime? filterByEndDate; + [ObservableProperty] private string? filterByMinAmount; + [ObservableProperty] private string? filterByMaxAmount; + [ObservableProperty] private TransactionCategoryModel? filterByCategory; public ObservableCollection TransactionTypeFilterOptions { get; } private const string AllTypesFilter = "Tüm Türler"; - // Form Özellikleri [ObservableProperty] private TransactionModel editableTransaction; + [ObservableProperty] private TransactionModel? selectedTransaction; + [ObservableProperty] private AccountModel? selectedAccountForForm; + private TransactionCategoryModel? _selectedCategoryForForm; public TransactionCategoryModel? SelectedCategoryForForm { @@ -110,7 +120,7 @@ public TransactionsViewModel(ILogger logger, IApiService _transactionStore = transactionStore; _accountStore = accountStore; - // Koleksiyonları başlat + FilteredTransactions = new ObservableCollection(); AllCategories = new ObservableCollection(); CategoriesForFilter = new ObservableCollection(); @@ -133,7 +143,6 @@ public TransactionsViewModel(ILogger logger, IApiService NewTransaction(); } - // Filtreleme Tetikleyicileri partial void OnFilterByDescriptionChanged(string? value) => ApplyFilters(); partial void OnFilterByTransactionTypeChanged(string? value) => ApplyFilters(); partial void OnFilterByStartDateChanged(DateTime? value) => ApplyFilters(); @@ -247,7 +256,6 @@ private void CalculateTotals() { _logger.LogInformation($"CalculateTotals başladı. FilteredTransactions Count: {FilteredTransactions?.Count ?? 0}"); - // Null kontrolü if (FilteredTransactions == null || !FilteredTransactions.Any()) { _logger.LogWarning("FilteredTransactions boş veya null!"); @@ -266,7 +274,6 @@ private void CalculateTotals() _logger.LogInformation($"Gelir toplamı: {incomeTotal}, Gider toplamı: {expenseTotal}"); - // --- 1. Gelir / Gider Oranı (Ana Grafik) --- var incomeVsExpenseList = new List(); if (incomeTotal > 0) @@ -300,7 +307,6 @@ private void CalculateTotals() IncomeVsExpenseSeries = incomeVsExpenseList.ToArray(); _logger.LogInformation($"Ana grafik oluşturuldu, eleman sayısı: {incomeVsExpenseList.Count}"); - // --- 2. Gelirlerin Kategorilere Göre Dağılımı --- var incomeByCategory = incomeTransactions .Where(t => t.Amount > 0) .GroupBy(t => string.IsNullOrEmpty(t.CategoryName) ? "Kategorisiz" : t.CategoryName) @@ -328,8 +334,7 @@ private void CalculateTotals() DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = p => $"₺{p.Coordinate.PrimaryValue:N0}", DataLabelsSize = 11, - // Küçük dilimler için - //MinGeometrySize = 10 + MiniatureShapeSize = 10 }); _logger.LogInformation($"Gelir Kategorisi eklendi: {item.Category} - ₺{item.Total:N0}"); @@ -337,7 +342,6 @@ private void CalculateTotals() } else { - // Veri yoksa placeholder incomeSeries.Add(new PieSeries { Name = "Veri Yok", @@ -352,7 +356,6 @@ private void CalculateTotals() IncomeByCategorySeries = incomeSeries.ToArray(); _logger.LogInformation($"Gelir kategorileri grafiği oluşturuldu: {incomeSeries.Count} seri"); - // --- 3. Giderlerin Kategorilere Göre Dağılımı --- var expenseByCategory = expenseTransactions .Where(t => t.Amount > 0) .GroupBy(t => string.IsNullOrEmpty(t.CategoryName) ? "Kategorisiz" : t.CategoryName) @@ -380,8 +383,7 @@ private void CalculateTotals() DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Middle, DataLabelsFormatter = p => $"₺{p.Coordinate.PrimaryValue:N0}", DataLabelsSize = 11, - // Küçük dilimler için - //MinGeometrySize = 10 + MiniatureShapeSize = 10 }); _logger.LogInformation($"Gider Kategorisi eklendi: {item.Category} - ₺{item.Total:N0}"); @@ -389,7 +391,6 @@ private void CalculateTotals() } else { - // Veri yoksa placeholder expenseSeries.Add(new PieSeries { Name = "Veri Yok", @@ -404,7 +405,6 @@ private void CalculateTotals() ExpenseByCategorySeries = expenseSeries.ToArray(); _logger.LogInformation($"Gider kategorileri grafiği oluşturuldu: {expenseSeries.Count} seri"); - // Property değişikliği bildirimlerini manuel tetikle OnPropertyChanged(nameof(IncomeVsExpenseSeries)); OnPropertyChanged(nameof(IncomeByCategorySeries)); OnPropertyChanged(nameof(ExpenseByCategorySeries)); diff --git a/FinTrack/ViewModels/VideoRecorderViewModel.cs b/FinTrack/ViewModels/VideoRecorderViewModel.cs index 5e1fa4a..87bde2e 100644 --- a/FinTrack/ViewModels/VideoRecorderViewModel.cs +++ b/FinTrack/ViewModels/VideoRecorderViewModel.cs @@ -1,7 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FinTrackForWindows.Models.Debt; +using FinTrackForWindows.Services.AppInNotifications; using FinTrackForWindows.Services.Camera; +using Microsoft.Extensions.Logging; using System.Windows.Media.Imaging; namespace FinTrackForWindows.ViewModels @@ -26,18 +28,31 @@ public partial class VideoRecorderViewModel : ObservableObject public string RecordButtonText => IsRecording ? "Stop Recording" : "Start Recording"; public string CommitmentText { get; } - public VideoRecorderViewModel(DebtModel debt, ICameraService cameraService, Action closeWindowAction) + private readonly ILogger _logger; + private readonly IAppInNotificationService _appInNotificationService; + + public VideoRecorderViewModel(DebtModel debt, + ICameraService cameraService, + Action closeWindowAction, + ILogger logger, + IAppInNotificationService appInNotificationService) { _cameraService = cameraService; _closeWindowAction = closeWindowAction; - CommitmentText = $"Ben, {debt.BorrowerName}, {debt.LenderName} kişisinden {DateTime.UtcNow:dd.MM.yyyy} tarihinde almış olduğum {debt.Amount:N2} TRY tutarındaki borcu, en geç {debt.DueDate:dd.MM.yyyy} tarihinde ödemeyi taahhüt ediyorum. Eğer borcu belirtilen zamanda ve miktarda geri ödemezsem, bu video kaydının borç veren {debt.LenderName} kişisinin erişimine açılacağını ve yasal delil olarak kullanılabileceğini kabul ediyorum."; + _logger = logger; + _appInNotificationService = appInNotificationService; + + CommitmentText = $"I, {debt.BorrowerName}, acknowledge that I have received a loan in the amount of {debt.Amount:N2} {debt.Currency} from {debt.LenderName} on {DateTime.UtcNow:dd.MM.yyyy}. " + + $"I undertake to make the payment no later than {debt.DueDate:dd.MM.yyyy}. If I fail to repay the debt in the specified amount and on the specified date, " + + $"I acknowledge that this video recording will be made available to the lender {debt.LenderName} and may be used as legal evidence."; _cameraService.OnFrameReady = (frame) => CameraFrame = frame; if (!_cameraService.InitializeCamera()) { - // TODO: Show a user-friendly error message. + _logger.LogError("There was a problem while recording the video."); + _appInNotificationService.ShowError("There was a problem while recording the video."); _closeWindowAction(false, null); } } @@ -76,7 +91,6 @@ private void Cancel() [RelayCommand] private void Cleanup() { - // Bu metot, pencere kapandığında çağrılır. _cameraService.Release(); } } diff --git a/FinTrack/Views/BottomBarView.xaml b/FinTrack/Views/BottomBarView.xaml index 6fba4c4..30899bb 100644 --- a/FinTrack/Views/BottomBarView.xaml +++ b/FinTrack/Views/BottomBarView.xaml @@ -52,7 +52,8 @@ Margin="0,0,20,0"> + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + - - + + - - + + + + + + + + + + + + + + - - diff --git a/FinTrack/Views/TransactionsView.xaml b/FinTrack/Views/TransactionsView.xaml index 6c71505..eeaff1c 100644 --- a/FinTrack/Views/TransactionsView.xaml +++ b/FinTrack/Views/TransactionsView.xaml @@ -24,58 +24,52 @@ - - - + + LegendPosition="Bottom" + Height="280" + AnimationsSpeed="00:00:01" + LegendTextSize="12"/> - - + + LegendPosition="Bottom" + Height="280" + AnimationsSpeed="00:00:01" + LegendTextSize="10"/> - - + + LegendPosition="Bottom" + Height="280" + AnimationsSpeed="00:00:01" + LegendTextSize="10"/> - - @@ -89,41 +83,40 @@ - + - + - + - + - + - + - + - - @@ -167,24 +160,23 @@ - - - + - + - + - + @@ -192,18 +184,18 @@ - + - - +