diff --git a/FinTrack/Dtos/CategoryDtos/CategoryCreateDto.cs b/FinTrack/Dtos/CategoryDtos/CategoryCreateDto.cs new file mode 100644 index 0000000..0b006d5 --- /dev/null +++ b/FinTrack/Dtos/CategoryDtos/CategoryCreateDto.cs @@ -0,0 +1,7 @@ +namespace FinTrackForWindows.Dtos.CategoryDtos +{ + public class CategoryCreateDto + { + public string Name { get; set; } = null!; + } +} diff --git a/FinTrack/Dtos/CategoryDtos/CategoryDto.cs b/FinTrack/Dtos/CategoryDtos/CategoryDto.cs new file mode 100644 index 0000000..c660794 --- /dev/null +++ b/FinTrack/Dtos/CategoryDtos/CategoryDto.cs @@ -0,0 +1,10 @@ +namespace FinTrackForWindows.Dtos.CategoryDtos +{ + public class CategoryDto + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public DateTime CreatedAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + } +} diff --git a/FinTrack/Dtos/CategoryDtos/CategoryUpdateDto.cs b/FinTrack/Dtos/CategoryDtos/CategoryUpdateDto.cs new file mode 100644 index 0000000..c33d635 --- /dev/null +++ b/FinTrack/Dtos/CategoryDtos/CategoryUpdateDto.cs @@ -0,0 +1,7 @@ +namespace FinTrackForWindows.Dtos.CategoryDtos +{ + public class CategoryUpdateDto + { + public string Name { get; set; } = null!; + } +} diff --git a/FinTrack/Dtos/DebtDtos/CreateDebtOfferRequestDto.cs b/FinTrack/Dtos/DebtDtos/CreateDebtOfferRequestDto.cs new file mode 100644 index 0000000..5dab63b --- /dev/null +++ b/FinTrack/Dtos/DebtDtos/CreateDebtOfferRequestDto.cs @@ -0,0 +1,17 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.DebtDtos +{ + public class CreateDebtOfferRequestDto + { + public string BorrowerEmail { get; set; } = null!; + + public decimal Amount { get; set; } + + public BaseCurrencyType CurrencyCode { get; set; } + + public DateTime DueDateUtc { get; set; } + + public string? Description { get; set; } + } +} diff --git a/FinTrack/Dtos/DebtDtos/DebtCreateDto.cs b/FinTrack/Dtos/DebtDtos/DebtCreateDto.cs new file mode 100644 index 0000000..6f9a862 --- /dev/null +++ b/FinTrack/Dtos/DebtDtos/DebtCreateDto.cs @@ -0,0 +1,13 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.DebtDtos +{ + public class DebtCreateDto + { + public string BorrowerEmail { get; set; } = null!; + public decimal Amount { get; set; } + public BaseCurrencyType Currency { get; set; } + public DateTime DueDate { get; set; } + public string Description { get; set; } = string.Empty; + } +} diff --git a/FinTrack/Dtos/DebtDtos/DebtDto.cs b/FinTrack/Dtos/DebtDtos/DebtDto.cs new file mode 100644 index 0000000..f05ea6c --- /dev/null +++ b/FinTrack/Dtos/DebtDtos/DebtDto.cs @@ -0,0 +1,28 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.DebtDtos +{ + public class DebtDto + { + public int Id { get; set; } + public int LenderId { get; set; } + public string LenderName { get; set; } = null!; + public string LenderEmail { get; set; } = null!; + public string? LenderProfilePicture { get; set; } + public int BorrowerId { get; set; } + public string BorrowerName { get; set; } = null!; + public string BorrowerEmail { get; set; } = null!; + public string? BorrowerProfilePicture { get; set; } + public decimal Amount { get; set; } + public BaseCurrencyType Currency { get; set; } + public DateTime DueDateUtc { get; set; } + public string Description { get; set; } = null!; + public DebtStatusType Status { get; set; } + public DateTime CreateAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + public DateTime? PaidAtUtc { get; set; } + public DateTime? OperatorApprovalAtUtc { get; set; } + public DateTime? BorrowerApprovalAtUtc { get; set; } + public DateTime? PaymentConfirmationAtUtc { get; set; } + } +} diff --git a/FinTrack/Dtos/DebtDtos/DebtUpdateDto.cs b/FinTrack/Dtos/DebtDtos/DebtUpdateDto.cs new file mode 100644 index 0000000..ace4834 --- /dev/null +++ b/FinTrack/Dtos/DebtDtos/DebtUpdateDto.cs @@ -0,0 +1,9 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.DebtDtos +{ + public class DebtUpdateDto + { + public DebtStatusType Status { get; set; } + } +} diff --git a/FinTrack/Dtos/FeedbackDtos/FeedbackCreateDto.cs b/FinTrack/Dtos/FeedbackDtos/FeedbackCreateDto.cs new file mode 100644 index 0000000..137e580 --- /dev/null +++ b/FinTrack/Dtos/FeedbackDtos/FeedbackCreateDto.cs @@ -0,0 +1,12 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.FeedbackDtos +{ + public class FeedbackCreateDto + { + public string Subject { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public FeedbackType? Type { get; set; } + public string? SavedFilePath { get; set; } + } +} diff --git a/FinTrack/Dtos/FeedbackDtos/FeedbackDto.cs b/FinTrack/Dtos/FeedbackDtos/FeedbackDto.cs new file mode 100644 index 0000000..fb27132 --- /dev/null +++ b/FinTrack/Dtos/FeedbackDtos/FeedbackDto.cs @@ -0,0 +1,15 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.FeedbackDtos +{ + public class FeedbackDto + { + public int Id { get; set; } + public string Subject { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public FeedbackType? Type { get; set; } + public string? SavedFilePath { get; set; } + public DateTime CreatedAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + } +} diff --git a/FinTrack/Dtos/NotificationDtos/NotificationDto.cs b/FinTrack/Dtos/NotificationDtos/NotificationDto.cs new file mode 100644 index 0000000..648ef07 --- /dev/null +++ b/FinTrack/Dtos/NotificationDtos/NotificationDto.cs @@ -0,0 +1,16 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.NotificationDtos +{ + public class NotificationDto + { + public int Id { get; set; } + public string MessageHead { get; set; } = null!; + public string MessageBody { get; set; } = null!; + public NotificationType NotificationType { get; set; } + public bool IsRead { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? ReadAt { get; set; } + } +} diff --git a/FinTrack/Dtos/ReportDtos/ReportRequestDto.cs b/FinTrack/Dtos/ReportDtos/ReportRequestDto.cs new file mode 100644 index 0000000..f90e4ab --- /dev/null +++ b/FinTrack/Dtos/ReportDtos/ReportRequestDto.cs @@ -0,0 +1,30 @@ +using FinTrackForWindows.Enums; + + +namespace FinTrackForWindows.Dtos.ReportDtos +{ + public class ReportRequestDto + { + public ReportType ReportType { get; set; } + public DocumentFormat ExportFormat { get; set; } + + // Genel Filtreler + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public List? SelectedCategoryIds { get; set; } + public List? SelectedAccountIds { get; set; } + + // Bütçe Raporuna Özel + public List? SelectedBudgetIds { get; set; } + + // Hesap Raporuna Özel + public decimal? MinBalance { get; set; } + public decimal? MaxBalance { get; set; } + + // İşlem Raporuna Özel + public DateTime? Date { get; set; } + public bool IsIncomeSelected { get; set; } + public bool IsExpenseSelected { get; set; } + public string? SelectedSortingCriterion { get; set; } // Örn: "Amount_Asc", "Date_Desc" + } +} diff --git a/FinTrack/Dtos/SettingsDtos/ProfileSettingsDto.cs b/FinTrack/Dtos/SettingsDtos/ProfileSettingsDto.cs new file mode 100644 index 0000000..1a27a8f --- /dev/null +++ b/FinTrack/Dtos/SettingsDtos/ProfileSettingsDto.cs @@ -0,0 +1,12 @@ +namespace FinTrackForWindows.Dtos.SettingsDtos +{ + public class ProfileSettingsDto + { + public int Id { get; set; } + public string FullName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string? ProfilePictureUrl { get; set; } + public DateTime CreatedAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + } +} diff --git a/FinTrack/Dtos/SettingsDtos/ProfileSettingsUpdateDto.cs b/FinTrack/Dtos/SettingsDtos/ProfileSettingsUpdateDto.cs new file mode 100644 index 0000000..01dc96b --- /dev/null +++ b/FinTrack/Dtos/SettingsDtos/ProfileSettingsUpdateDto.cs @@ -0,0 +1,9 @@ +namespace FinTrackForWindows.Dtos.SettingsDtos +{ + public class ProfileSettingsUpdateDto + { + public string? FullName { get; set; } + public string? Email { get; set; } + public string? ProfilePictureUrl { get; set; } + } +} diff --git a/FinTrack/Dtos/SettingsDtos/UserAppSettingsDto.cs b/FinTrack/Dtos/SettingsDtos/UserAppSettingsDto.cs new file mode 100644 index 0000000..7626a81 --- /dev/null +++ b/FinTrack/Dtos/SettingsDtos/UserAppSettingsDto.cs @@ -0,0 +1,13 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.SettingsDtos +{ + public class UserAppSettingsDto + { + public int Id { get; set; } + public AppearanceType Appearance { get; set; } + public BaseCurrencyType Currency { get; set; } + public DateTime CreatedAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + } +} diff --git a/FinTrack/Dtos/SettingsDtos/UserAppSettingsUpdateDto.cs b/FinTrack/Dtos/SettingsDtos/UserAppSettingsUpdateDto.cs new file mode 100644 index 0000000..9a5d025 --- /dev/null +++ b/FinTrack/Dtos/SettingsDtos/UserAppSettingsUpdateDto.cs @@ -0,0 +1,10 @@ +using FinTrackForWindows.Enums; + +namespace FinTrackForWindows.Dtos.SettingsDtos +{ + public class UserAppSettingsUpdateDto + { + public AppearanceType Appearance { get; set; } + public BaseCurrencyType Currency { get; set; } + } +} diff --git a/FinTrack/Dtos/SettingsDtos/UserNotificationSettingsDto.cs b/FinTrack/Dtos/SettingsDtos/UserNotificationSettingsDto.cs new file mode 100644 index 0000000..fddc89a --- /dev/null +++ b/FinTrack/Dtos/SettingsDtos/UserNotificationSettingsDto.cs @@ -0,0 +1,14 @@ +namespace FinTrackForWindows.Dtos.SettingsDtos +{ + public class UserNotificationSettingsDto + { + public int Id { get; set; } + public bool SpendingLimitWarning { get; set; } + public bool ExpectedBillReminder { get; set; } + public bool WeeklySpendingSummary { get; set; } + public bool NewFeaturesAndAnnouncements { get; set; } + public bool EnableDesktopNotifications { get; set; } + public DateTime CreatedAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + } +} diff --git a/FinTrack/Dtos/SettingsDtos/UserNotificationSettingsUpdateDto.cs b/FinTrack/Dtos/SettingsDtos/UserNotificationSettingsUpdateDto.cs new file mode 100644 index 0000000..88ca16a --- /dev/null +++ b/FinTrack/Dtos/SettingsDtos/UserNotificationSettingsUpdateDto.cs @@ -0,0 +1,11 @@ +namespace FinTrackForWindows.Dtos.SettingsDtos +{ + public class UserNotificationSettingsUpdateDto + { + public bool SpendingLimitWarning { get; set; } + public bool ExpectedBillReminder { get; set; } + public bool WeeklySpendingSummary { get; set; } + public bool NewFeaturesAndAnnouncements { get; set; } + public bool EnableDesktopNotifications { get; set; } + } +} diff --git a/FinTrack/Enums/CurrencyConversionType.cs b/FinTrack/Enums/CurrencyConversionType.cs index 9ddad28..3fddd19 100644 --- a/FinTrack/Enums/CurrencyConversionType.cs +++ b/FinTrack/Enums/CurrencyConversionType.cs @@ -1,4 +1,9 @@ namespace FinTrackForWindows.Enums { - public enum CurrencyConversionType { Increase, Decrease } + public enum CurrencyConversionType + { + Increase, + Decrease, + Neutral + } } diff --git a/FinTrack/Enums/DebtStatus.cs b/FinTrack/Enums/DebtStatus.cs index 5a78272..0283992 100644 --- a/FinTrack/Enums/DebtStatus.cs +++ b/FinTrack/Enums/DebtStatus.cs @@ -2,14 +2,15 @@ { public enum DebtStatusType { - PendingBorrowerAcceptance, // Borç Alan Onayı Bekliyor - PendingOperatorApproval, // Operatör Onayı Bekliyor (eğer varsa) - Active, // Aktif Borç - PaymentConfirmationPending, // Ödeme Onayı Bekliyor - Paid, // Ödendi - Defaulted, // Vadesi Geçmiş/Ödenmemiş - RejectedByBorrower, // Borç Alan Tarafından Reddedildi - RejectedByOperator, // Operatör Tarafından Reddedildi (eğer varsa) - CancelledByLender, // Borç Veren Tarafından İptal Edildi + PendingBorrowerAcceptance, // Borç Alan Onayı Bekliyor + AcceptedPendingVideoUpload, // Borçlu Kabul Etti, Video Yüklemesi Bekleniyor (YENİ) + PendingOperatorApproval, // Operatör Onayı Bekliyor + Active, // Aktif Borç + PaymentConfirmationPending, // Ödeme Onayı Bekliyor + Paid, // Ödendi + Defaulted, // Vadesi Geçmiş/Ödenmemiş + RejectedByBorrower, // Borç Alan Tarafından Reddedildi + RejectedByOperator, // Operatör Tarafından Reddedildi + CancelledByLender, // Borç Veren Tarafından İptal Edildi } } diff --git a/FinTrack/Enums/ExportFormat.cs b/FinTrack/Enums/ExportFormat.cs index e60e973..260b10d 100644 --- a/FinTrack/Enums/ExportFormat.cs +++ b/FinTrack/Enums/ExportFormat.cs @@ -1,6 +1,6 @@ namespace FinTrackForWindows.Enums { - public enum ExportFormat + public enum DocumentFormat { PDF, Word, diff --git a/FinTrack/Enums/FeedbackTypes.cs b/FinTrack/Enums/FeedbackType.cs similarity index 91% rename from FinTrack/Enums/FeedbackTypes.cs rename to FinTrack/Enums/FeedbackType.cs index c310365..af1913c 100644 --- a/FinTrack/Enums/FeedbackTypes.cs +++ b/FinTrack/Enums/FeedbackType.cs @@ -2,7 +2,7 @@ namespace FinTrackForWindows.Enums { - public enum FeedbackTypes + public enum FeedbackType { [Description("Bug Report")] BugReport, diff --git a/FinTrack/Enums/NotificationType.cs b/FinTrack/Enums/NotificationType.cs index a5627b8..873e3f3 100644 --- a/FinTrack/Enums/NotificationType.cs +++ b/FinTrack/Enums/NotificationType.cs @@ -2,9 +2,9 @@ { public enum NotificationType { - Suggestion, - GoalAchieved, + Info, Warning, - General + Error, + Success, } } diff --git a/FinTrack/FinTrackForWindows.csproj b/FinTrack/FinTrackForWindows.csproj index 19aaa5b..8e515c1 100644 --- a/FinTrack/FinTrackForWindows.csproj +++ b/FinTrack/FinTrackForWindows.csproj @@ -53,6 +53,7 @@ + diff --git a/FinTrack/Helpers/BusyAdorner.cs b/FinTrack/Helpers/BusyAdorner.cs new file mode 100644 index 0000000..bbb4900 --- /dev/null +++ b/FinTrack/Helpers/BusyAdorner.cs @@ -0,0 +1,62 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace FinTrackForWindows.Helpers +{ + public class BusyAdorner : Adorner + { + private readonly StackPanel _child; + + public BusyAdorner(UIElement adornedElement) : base(adornedElement) + { + var progressBar = new ProgressBar + { + IsIndeterminate = true, + Width = 100, + Height = 20 + }; + + var textBlock = new TextBlock + { + Text = "Creating Report...", + Foreground = Brushes.White, + Margin = new Thickness(0, 15, 0, 0), + FontSize = 14, + HorizontalAlignment = HorizontalAlignment.Center + }; + + _child = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + _child.Children.Add(progressBar); + _child.Children.Add(textBlock); + + AddVisualChild(_child); + } + + protected override Size MeasureOverride(Size constraint) + { + AdornedElement.Measure(constraint); + return AdornedElement.DesiredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + _child.Arrange(new Rect(finalSize)); + return finalSize; + } + + protected override void OnRender(DrawingContext drawingContext) + { + drawingContext.DrawRectangle(new SolidColorBrush(Color.FromArgb(160, 0, 0, 0)), null, new Rect(RenderSize)); + base.OnRender(drawingContext); + } + + protected override int VisualChildrenCount => 1; + protected override Visual GetVisualChild(int index) => _child; + } +} \ No newline at end of file diff --git a/FinTrack/Helpers/EnumMatchToBooleanConverter.cs b/FinTrack/Helpers/EnumMatchToBooleanConverter.cs new file mode 100644 index 0000000..9822b35 --- /dev/null +++ b/FinTrack/Helpers/EnumMatchToBooleanConverter.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class EnumMatchToBooleanConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values == null || values.Length < 2) + return false; + + var value = values[0]; + var targetValue = values[1]; + + if (value == null || targetValue == null) + return false; + + return value.Equals(targetValue); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + if (value is bool && (bool)value) + { + return new object[] { Binding.DoNothing, value }; + } + return new object[] { Binding.DoNothing, Binding.DoNothing }; + } + } +} \ No newline at end of file diff --git a/FinTrack/Helpers/FileSaver.cs b/FinTrack/Helpers/FileSaver.cs new file mode 100644 index 0000000..6c70002 --- /dev/null +++ b/FinTrack/Helpers/FileSaver.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using System.IO; + +namespace FinTrackForWindows.Helpers +{ + public static class FileSaver + { + public static async Task SaveReportToDocumentsAsync(byte[] fileBytes, string fileName) + { + string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + + string targetFolderPath = Path.Combine(documentsPath, "FinTrackReport"); + + Directory.CreateDirectory(targetFolderPath); + + string fullFilePath = Path.Combine(targetFolderPath, fileName); + + await File.WriteAllBytesAsync(fullFilePath, fileBytes); + + return fullFilePath; + } + + public static void OpenContainingFolder(string filePath) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) return; + + Process.Start("explorer.exe", $"/select,\"{filePath}\""); + } + } +} \ No newline at end of file diff --git a/FinTrack/Helpers/InverseCountToVisibilityConverter.cs b/FinTrack/Helpers/InverseCountToVisibilityConverter.cs new file mode 100644 index 0000000..65c9ab9 --- /dev/null +++ b/FinTrack/Helpers/InverseCountToVisibilityConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class InverseCountToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int count) + { + return count == 0 ? Visibility.Visible : Visibility.Collapsed; + } + 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/InvertedBooleanConverter.cs b/FinTrack/Helpers/InvertedBooleanConverter.cs new file mode 100644 index 0000000..6518c99 --- /dev/null +++ b/FinTrack/Helpers/InvertedBooleanConverter.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class InvertedBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) return !b; + return true; + } + + 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/IsBusyAdornerBehavior.cs b/FinTrack/Helpers/IsBusyAdornerBehavior.cs new file mode 100644 index 0000000..44d24c0 --- /dev/null +++ b/FinTrack/Helpers/IsBusyAdornerBehavior.cs @@ -0,0 +1,53 @@ +using System.Windows; +using System.Windows.Documents; + +namespace FinTrackForWindows.Helpers +{ + public static class IsBusyAdornerBehavior + { + public static readonly DependencyProperty IsBusyProperty = + DependencyProperty.RegisterAttached( + "IsBusy", + typeof(bool), + typeof(IsBusyAdornerBehavior), + new FrameworkPropertyMetadata(false, OnIsBusyChanged)); + + public static bool GetIsBusy(DependencyObject d) + { + return (bool)d.GetValue(IsBusyProperty); + } + + public static void SetIsBusy(DependencyObject d, bool value) + { + d.SetValue(IsBusyProperty, value); + } + + private static void OnIsBusyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not UIElement element) return; + + var adornerLayer = AdornerLayer.GetAdornerLayer(element); + if (adornerLayer == null) return; + + if ((bool)e.NewValue) + { + var adorner = new BusyAdorner(element); + adornerLayer.Add(adorner); + } + else + { + var adorners = adornerLayer.GetAdorners(element); + if (adorners != null) + { + foreach (var adorner in adorners) + { + if (adorner is BusyAdorner) + { + adornerLayer.Remove(adorner); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/FinTrack/Helpers/ObjectArrayConverter.cs b/FinTrack/Helpers/ObjectArrayConverter.cs new file mode 100644 index 0000000..c68e66e --- /dev/null +++ b/FinTrack/Helpers/ObjectArrayConverter.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using System.Windows.Data; + +namespace FinTrackForWindows.Helpers +{ + public class ObjectArrayConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + return values.Clone(); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/FinTrack/Helpers/TextBoxMaskBehavior.cs b/FinTrack/Helpers/TextBoxMaskBehavior.cs new file mode 100644 index 0000000..88660df --- /dev/null +++ b/FinTrack/Helpers/TextBoxMaskBehavior.cs @@ -0,0 +1,73 @@ +using Microsoft.Xaml.Behaviors; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace FinTrackForWindows.Helpers +{ + public class TextBoxMaskBehavior : Behavior + { + public static readonly System.Windows.DependencyProperty MaskProperty = + System.Windows.DependencyProperty.Register("Mask", typeof(string), typeof(TextBoxMaskBehavior), new System.Windows.PropertyMetadata(null)); + + public string Mask + { + get { return (string)GetValue(MaskProperty); } + set { SetValue(MaskProperty, value); } + } + + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.PreviewTextInput += OnPreviewTextInput; + DataObject.AddPastingHandler(AssociatedObject, OnPasting); + } + + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject.PreviewTextInput -= OnPreviewTextInput; + DataObject.RemovePastingHandler(AssociatedObject, OnPasting); + } + + private void OnPasting(object sender, DataObjectPastingEventArgs e) + { + if (e.DataObject.GetDataPresent(typeof(string))) + { + var text = (string)e.DataObject.GetData(typeof(string)); + if (!IsTextAllowed(text)) + { + e.CancelCommand(); + } + } + else + { + e.CancelCommand(); + } + } + + private void OnPreviewTextInput(object sender, TextCompositionEventArgs e) + { + e.Handled = !IsTextAllowed(e.Text); + } + + private bool IsTextAllowed(string text) + { + if (string.IsNullOrEmpty(Mask)) return true; + + switch (Mask.ToLower()) + { + case "integer": + return Regex.IsMatch(text, "[0-9]"); + case "decimal": + // Sadece sayı ve tek bir ondalık ayırıcıya izin ver + var currentText = AssociatedObject.Text; + var futureText = currentText.Insert(AssociatedObject.CaretIndex, text); + return Regex.IsMatch(futureText, @"^\d*\.?\d*$"); + default: + return Regex.IsMatch(text, Mask); + } + } + } +} \ No newline at end of file diff --git a/FinTrack/Models/Currency/CurrencyModel.cs b/FinTrack/Models/Currency/CurrencyModel.cs index 5a4769d..a1be66d 100644 --- a/FinTrack/Models/Currency/CurrencyModel.cs +++ b/FinTrack/Models/Currency/CurrencyModel.cs @@ -21,26 +21,34 @@ public partial class CurrencyModel : ObservableObject private decimal toCurrencyPrice; [ObservableProperty] - private string toCurrencyChange = string.Empty; + private string toCurrencyChange = "N/A"; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ToCurrencyChangeForeground))] - private CurrencyConversionType type = CurrencyConversionType.Increase; + private CurrencyConversionType type = CurrencyConversionType.Neutral; [ObservableProperty] - private string dailyLow = string.Empty; + private string dailyLow = "N/A"; [ObservableProperty] - private string dailyHigh = string.Empty; + private string dailyHigh = "N/A"; [ObservableProperty] - private string weeklyChange = string.Empty; + private string weeklyChange = "N/A"; [ObservableProperty] - private string monthlyChange = string.Empty; + [NotifyPropertyChangedFor(nameof(WeeklyChangeForeground))] + private CurrencyConversionType weeklyChangeType = CurrencyConversionType.Neutral; - private static readonly Brush IncreaseBrush = new SolidColorBrush(Colors.Green); - private static readonly Brush DecreaseBrush = new SolidColorBrush(Colors.Red); + [ObservableProperty] + private string monthlyChange = "N/A"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(MonthlyChangeForeground))] + private CurrencyConversionType monthlyChangeType = CurrencyConversionType.Neutral; + + private static readonly Brush IncreaseBrush = new SolidColorBrush(Color.FromRgb(46, 204, 113)); + private static readonly Brush DecreaseBrush = new SolidColorBrush(Color.FromRgb(231, 76, 60)); private static readonly Brush DefaultBrush = new SolidColorBrush(Colors.Gray); public Brush ToCurrencyChangeForeground => Type switch @@ -49,5 +57,19 @@ public partial class CurrencyModel : ObservableObject CurrencyConversionType.Decrease => DecreaseBrush, _ => DefaultBrush }; + + public Brush WeeklyChangeForeground => WeeklyChangeType switch + { + CurrencyConversionType.Increase => IncreaseBrush, + CurrencyConversionType.Decrease => DecreaseBrush, + _ => DefaultBrush + }; + + public Brush MonthlyChangeForeground => MonthlyChangeType switch + { + CurrencyConversionType.Increase => IncreaseBrush, + CurrencyConversionType.Decrease => DecreaseBrush, + _ => DefaultBrush + }; } -} +} \ No newline at end of file diff --git a/FinTrack/Models/Dashboard/ReportDashboardModel.cs b/FinTrack/Models/Dashboard/ReportDashboardModel.cs index d6b06aa..80f7857 100644 --- a/FinTrack/Models/Dashboard/ReportDashboardModel.cs +++ b/FinTrack/Models/Dashboard/ReportDashboardModel.cs @@ -12,7 +12,7 @@ public partial class ReportDashboardModel : ObservableObject [ObservableProperty] private string name = string.Empty; - public ObservableCollection Formats { get; set; } + public ObservableCollection Formats { get; set; } private readonly ILogger _logger; @@ -20,16 +20,16 @@ public ReportDashboardModel(ILogger logger) { _logger = logger; - Formats = new ObservableCollection(); + Formats = new ObservableCollection(); - foreach (ExportFormat exportFormat in Enum.GetValues(typeof(ExportFormat))) + foreach (DocumentFormat exportFormat in Enum.GetValues(typeof(DocumentFormat))) { Formats.Add(exportFormat); } } [RelayCommand] - private void Generate(ExportFormat format) + private void Generate(DocumentFormat format) { _logger.LogInformation("Rapor oluşturuluyor -> Rapor Adı: {ReportName}, Format: {Format}", this.Name, format); diff --git a/FinTrack/Models/Debt/DebtModel.cs b/FinTrack/Models/Debt/DebtModel.cs index fd6a6ab..23f7add 100644 --- a/FinTrack/Models/Debt/DebtModel.cs +++ b/FinTrack/Models/Debt/DebtModel.cs @@ -6,82 +6,72 @@ namespace FinTrackForWindows.Models.Debt { public partial class DebtModel : ObservableObject { + public int Id { get; set; } + public int LenderId { get; set; } + public int BorrowerId { get; set; } + public int CurrentUserId { get; set; } + [ObservableProperty] private string lenderName = string.Empty; [ObservableProperty] private string borrowerName = string.Empty; - [ObservableProperty] - private string borrowerEmail = string.Empty; - [ObservableProperty] private decimal amount; [ObservableProperty] private DateTime dueDate; - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(StatusText))] - [NotifyPropertyChangedFor(nameof(StatusBrush))] - [NotifyPropertyChangedFor(nameof(IsActionRequired))] - [NotifyPropertyChangedFor(nameof(IsRejected))] - private DebtStatusType status; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(DebtTitle))] - [NotifyPropertyChangedFor(nameof(UserIconPath))] - [NotifyPropertyChangedFor(nameof(IsCurrentUserTheBorrower))] - private string currentUserName = string.Empty; - - public string DebtTitle => IsCurrentUserTheBorrower ? $"Your debt: {LenderName}" : $"Debt owed to you: {BorrowerName}"; + public string borrowerImageUrl = string.Empty; - public string UserIconPath => IsCurrentUserTheBorrower ? "/Assets/Images/Icons/user-red.png" : "/Assets/Images/Icons/user-green.png"; - - public bool IsCurrentUserTheBorrower => BorrowerName == CurrentUserName; + public string lenderImageUrl = string.Empty; [ObservableProperty] - private string? rejectionReason; + [NotifyPropertyChangedFor(nameof(StatusText), nameof(StatusBrush), nameof(IsActionRequiredForBorrower), nameof(IsRejected))] + private DebtStatusType status; - [ObservableProperty] - private DateTime createdDate = DateTime.Now; + public bool IsCurrentUserTheBorrower => BorrowerId == CurrentUserId; + public bool IsCurrentUserTheLender => LenderId == CurrentUserId; + + public string DebtTitle => IsCurrentUserTheBorrower + ? $"Alacaklı: {LenderName}" + : $"Borçlu: {BorrowerName}"; - private static readonly Brush GreenBrush = new SolidColorBrush(Colors.Green); - private static readonly Brush RedBrush = new SolidColorBrush(Colors.Red); - private static readonly Brush BlueBrush = new SolidColorBrush(Colors.Blue); - private static readonly Brush OrangeBrush = new SolidColorBrush(Colors.Orange); - private static readonly Brush GrayBrush = new SolidColorBrush(Colors.Gray); + public string UserIconPath => IsCurrentUserTheBorrower + ? "/Assets/Images/Icons/user-red.png" + : "/Assets/Images/Icons/user-green.png"; public Brush StatusBrush => Status switch { - DebtStatusType.Active => GreenBrush, - DebtStatusType.PendingBorrowerAcceptance => BlueBrush, - DebtStatusType.PendingOperatorApproval => OrangeBrush, - DebtStatusType.RejectedByOperator => RedBrush, - DebtStatusType.RejectedByBorrower => RedBrush, - _ => GrayBrush + DebtStatusType.Active => new SolidColorBrush(Colors.Green), + DebtStatusType.Defaulted => new SolidColorBrush(Colors.DarkRed), + DebtStatusType.AcceptedPendingVideoUpload => new SolidColorBrush(Colors.CornflowerBlue), + DebtStatusType.PendingBorrowerAcceptance => new SolidColorBrush(Colors.DodgerBlue), + DebtStatusType.PendingOperatorApproval => new SolidColorBrush(Colors.Orange), + DebtStatusType.RejectedByOperator => new SolidColorBrush(Colors.Red), + DebtStatusType.RejectedByBorrower => new SolidColorBrush(Colors.Red), + _ => new SolidColorBrush(Colors.Gray) }; public string StatusText => Status switch { - DebtStatusType.PendingBorrowerAcceptance => "Status: Awaiting Video Approval", - DebtStatusType.PendingOperatorApproval => "Status: FinTrack Operator Approval Pending", - DebtStatusType.Active => "Status: Active - In force", - DebtStatusType.RejectedByOperator => "Status: Rejected by Operator", - DebtStatusType.RejectedByBorrower => "Status: Rejected by Borrower", - _ => "Status: Unknown" + DebtStatusType.PendingBorrowerAcceptance => "Onayınız Bekleniyor", + DebtStatusType.AcceptedPendingVideoUpload => "Video Yüklemesi Bekleniyor", + DebtStatusType.PendingOperatorApproval => "Operatör Onayı Bekleniyor", + DebtStatusType.Active => "Aktif", + DebtStatusType.Paid => "Ödendi", + DebtStatusType.Defaulted => "Vadesi Geçmiş", + DebtStatusType.RejectedByBorrower => "Tarafınızdan Reddedildi", + DebtStatusType.RejectedByOperator => "Operatör Tarafından Reddedildi", + _ => "Bilinmeyen Durum" }; - public bool IsActionRequired => Status == DebtStatusType.PendingBorrowerAcceptance; + // Borçlunun video yüklemesi gerekip gerekmediğini kontrol eder. + public bool IsActionRequiredForBorrower => + Status == DebtStatusType.AcceptedPendingVideoUpload && IsCurrentUserTheBorrower; - public bool IsRejected => Status == DebtStatusType.RejectedByBorrower || Status == DebtStatusType.RejectedByOperator; - - public string InfoText => Status switch - { - DebtStatusType.Active => $"Final Payment: {DueDate:dd.MM.yyyy}", - DebtStatusType.PendingOperatorApproval => "Video uploaded", - DebtStatusType.RejectedByOperator => $"Reason: {RejectionReason}", - _ => string.Empty - }; + public bool IsRejected => + Status == DebtStatusType.RejectedByBorrower || Status == DebtStatusType.RejectedByOperator; } -} +} \ No newline at end of file diff --git a/FinTrack/Models/Notification/NotificationModel.cs b/FinTrack/Models/Notification/NotificationModel.cs index 6eef6b4..29010aa 100644 --- a/FinTrack/Models/Notification/NotificationModel.cs +++ b/FinTrack/Models/Notification/NotificationModel.cs @@ -5,6 +5,8 @@ namespace FinTrackForWindows.Models.Notification { public partial class NotificationModel : ObservableObject { + public int Id { get; set; } + [ObservableProperty] private string title = string.Empty; @@ -23,8 +25,9 @@ public partial class NotificationModel : ObservableObject public bool IsUnread => !IsRead; - public NotificationModel(string title, string message, string? timestamp, NotificationType type, bool _isRead = false) + public NotificationModel(int id, string title, string message, string? timestamp, NotificationType type, bool _isRead = false) { + Id = id; Title = title; Message = message; Type = type; diff --git a/FinTrack/Models/Report/SelectableOptionReport.cs b/FinTrack/Models/Report/SelectableOptionReport.cs index 52f54b3..9037435 100644 --- a/FinTrack/Models/Report/SelectableOptionReport.cs +++ b/FinTrack/Models/Report/SelectableOptionReport.cs @@ -4,16 +4,19 @@ namespace FinTrackForWindows.Models.Report { public partial class SelectableOptionReport : ObservableObject { + public int Id { get; } + [ObservableProperty] private string name; [ObservableProperty] private bool isSelected; - public SelectableOptionReport(string _name, bool _isSelected = false) + public SelectableOptionReport(int _id, string _name, bool _isSelected = false) { name = _name; isSelected = _isSelected; + Id = _id; } } } diff --git a/FinTrack/Services/Api/ApiService.cs b/FinTrack/Services/Api/ApiService.cs index 0117e8d..b3741d6 100644 --- a/FinTrack/Services/Api/ApiService.cs +++ b/FinTrack/Services/Api/ApiService.cs @@ -1,6 +1,8 @@ using FinTrackForWindows.Core; +using FinTrackForWindows.Dtos.ReportDtos; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; @@ -230,5 +232,110 @@ private void AddAuthorizationHeader() return default(T); } } + + public async Task UploadFileAsync(string endpoint, string filePath) + { + _logger.LogInformation("Dosya yükleme isteği başlatılıyor: {Endpoint}, Dosya: {FilePath}", endpoint, filePath); + if (!File.Exists(filePath)) + { + _logger.LogError("Yüklenecek dosya bulunamadı: {FilePath}", filePath); + return false; + } + + try + { + AddAuthorizationHeader(); + + using (var content = new MultipartFormDataContent()) + { + var fileBytes = await File.ReadAllBytesAsync(filePath); + var fileContent = new ByteArrayContent(fileBytes); + + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"); + + content.Add(fileContent, "file", Path.GetFileName(filePath)); + var response = await _httpClient.PostAsync(endpoint, content); + + if (response.IsSuccessStatusCode) + { + _logger.LogInformation("Dosya başarıyla yüklendi: {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); + return false; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Dosya yükleme sırasında genel bir hata oluştu: {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); + if (string.IsNullOrWhiteSpace(endpoint)) + { + _logger.LogError("Rapor indirme isteği sırasında endpoint boş veya null."); + throw new ArgumentException("Endpoint cannot be null or empty", nameof(endpoint)); + } + if (payload == null) + { + _logger.LogError("Rapor indirme isteği sırasında gönderilecek veri (payload) null."); + throw new ArgumentNullException(nameof(payload)); + } + + try + { + AddAuthorizationHeader(); + + var jsonContent = JsonContent.Create(payload, options: _jsonSerializerOptions); + + var response = await _httpClient.PostAsync(endpoint, jsonContent); + + if (response.IsSuccessStatusCode) + { + string fileName = response.Content.Headers.ContentDisposition?.FileName?.Trim('"'); + + if (string.IsNullOrEmpty(fileName)) + { + var format = "bin"; + if (payload is ReportRequestDto dto) + { + 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); + } + + byte[] fileBytes = await response.Content.ReadAsByteArrayAsync(); + + _logger.LogInformation("Rapor başarıyla indirildi: {FileName}, Boyut: {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); + + throw new HttpRequestException($"API'den rapor alınamadı. Sunucu '{response.StatusCode}' durum kodu ile cevap verdi. Detay: {errorContent}"); + } + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Rapor indirme sırasında HTTP isteği hatası: {Endpoint}", endpoint); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Rapor indirme sırasında genel bir hata oluştu: {Endpoint}", endpoint); + throw; + } + } } } diff --git a/FinTrack/Services/Api/IApiService.cs b/FinTrack/Services/Api/IApiService.cs index 96c374e..5a91a5e 100644 --- a/FinTrack/Services/Api/IApiService.cs +++ b/FinTrack/Services/Api/IApiService.cs @@ -6,5 +6,7 @@ public interface IApiService Task PostAsync(string endpoint, object data); Task PutAsync(string endpoint, object data); Task DeleteAsync(string endpoint); + Task UploadFileAsync(string endpoint, string filePath); + Task<(byte[] FileBytes, string FileName)?> PostAndDownloadReportAsync(string endpoint, T payload); } } diff --git a/FinTrack/Styles/ModernStyles.xaml b/FinTrack/Styles/ModernStyles.xaml index e884de6..672002c 100644 --- a/FinTrack/Styles/ModernStyles.xaml +++ b/FinTrack/Styles/ModernStyles.xaml @@ -999,5 +999,121 @@ + + + + + + + + + + \ No newline at end of file diff --git a/FinTrack/ViewModels/AccountViewModel.cs b/FinTrack/ViewModels/AccountViewModel.cs index 94cbdba..1393198 100644 --- a/FinTrack/ViewModels/AccountViewModel.cs +++ b/FinTrack/ViewModels/AccountViewModel.cs @@ -69,6 +69,12 @@ public AccountViewModel(ILogger logger, IApiService apiService { CurrencyTypes.Add(currencyType); } + + AccountTypes.Clear(); + foreach (AccountType accountType in Enum.GetValues(typeof(AccountType))) + { + AccountTypes.Add(accountType); + } } private void InitializeEmptyChart() diff --git a/FinTrack/ViewModels/AppSettingsContentViewModel.cs b/FinTrack/ViewModels/AppSettingsContentViewModel.cs index fbeb618..e3cb347 100644 --- a/FinTrack/ViewModels/AppSettingsContentViewModel.cs +++ b/FinTrack/ViewModels/AppSettingsContentViewModel.cs @@ -1,6 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.SettingsDtos; using FinTrackForWindows.Enums; +using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Windows; @@ -10,46 +12,94 @@ namespace FinTrackForWindows.ViewModels public partial class AppSettingsContentViewModel : ObservableObject { public ObservableCollection AppearanceTypes { get; } - public ObservableCollection CurrencyTypes { get; } [ObservableProperty] - private bool isFirstOpening = true; + private AppearanceType _selectedAppearanceType; + + [ObservableProperty] + private BaseCurrencyType _selectedCurrencyType; + + [ObservableProperty] + private bool _startWithWindows; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveChangesCommand))] + private bool _isSaving; private readonly ILogger _logger; + private readonly IApiService _apiService; - public AppSettingsContentViewModel(ILogger logger) + public AppSettingsContentViewModel(ILogger logger, IApiService apiService) { _logger = logger; - AppearanceTypes = new ObservableCollection(); - CurrencyTypes = new ObservableCollection(); + _apiService = apiService; - InitializeAppearanceTypes(); + AppearanceTypes = new ObservableCollection(Enum.GetValues()); + CurrencyTypes = new ObservableCollection(Enum.GetValues()); + + _ = LoadAppSettings(); } - private void InitializeAppearanceTypes() + private async Task LoadAppSettings() { - AppearanceTypes.Clear(); - foreach (AppearanceType appearanceType in Enum.GetValues(typeof(AppearanceType))) + IsLoading = true; + try { - AppearanceTypes.Add(appearanceType); + var settings = await _apiService.GetAsync("UserSettings/AppSettings"); + if (settings != null) + { + SelectedAppearanceType = settings.Appearance; + SelectedCurrencyType = settings.Currency; + // StartWithWindows = settings.StartWithWindows; // DTO'ya eklendiğinde bu satır açılmalı + } } - - CurrencyTypes.Clear(); - foreach (BaseCurrencyType currencyType in Enum.GetValues(typeof(BaseCurrencyType))) + catch (Exception ex) { - CurrencyTypes.Add(currencyType); + _logger.LogError(ex, "Failed to load application settings."); + MessageBox.Show("Could not load application settings. Default values will be used.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); + } + finally + { + IsLoading = false; } } - [RelayCommand] - private void AppSettingsContentChanges() + [RelayCommand(CanExecute = nameof(CanSaveChanges))] + private async Task SaveChanges() { - _logger.LogInformation("Kullanıcı uygulama ayarlarını değiştirdi. İlk açılış: {IsFirstOpening}", IsFirstOpening); - MessageBox.Show("Kullanıcı uygulama ayarlarını değiştirdi.", - "Uygulama Ayarları", - MessageBoxButton.OK, - MessageBoxImage.Information); + IsSaving = true; + try + { + var settingsToUpdate = new UserAppSettingsUpdateDto + { + Appearance = SelectedAppearanceType, + Currency = SelectedCurrencyType + // StartWithWindows = this.StartWithWindows; // DTO'ya eklendiğinde bu satır açılmalı + }; + + await _apiService.PostAsync("UserSettings/AppSettings", settingsToUpdate); + + // TODO: StartWithWindows ayarını gerçekten sisteme uygulayacak bir servis çağrısı burada yapılmalı. + // Örneğin: _startupService.SetStartup(this.StartWithWindows); + + _logger.LogInformation("Application settings have been updated by the user."); + MessageBox.Show("Settings saved successfully!", "Application Settings", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save application settings."); + MessageBox.Show("An error occurred while saving your settings. Please try again.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsSaving = false; + } } + + private bool CanSaveChanges() => !IsSaving; } } \ No newline at end of file diff --git a/FinTrack/ViewModels/CurrenciesViewModel.cs b/FinTrack/ViewModels/CurrenciesViewModel.cs index 9155f9f..fd66a05 100644 --- a/FinTrack/ViewModels/CurrenciesViewModel.cs +++ b/FinTrack/ViewModels/CurrenciesViewModel.cs @@ -3,95 +3,392 @@ using FinTrackForWindows.Models.Currency; using FinTrackForWindows.Services.Api; using FinTrackWebApi.Dtos.CurrencyDtos; +using LiveChartsCore; +using LiveChartsCore.Defaults; +using LiveChartsCore.Drawing; +using LiveChartsCore.Kernel; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Drawing; +using LiveChartsCore.SkiaSharpView.Painting; +using LiveChartsCore.SkiaSharpView.Painting.Effects; +using LiveChartsCore.SkiaSharpView.VisualElements; using Microsoft.Extensions.Logging; +using SkiaSharp; using System.Collections.ObjectModel; +using System.Globalization; namespace FinTrackForWindows.ViewModels { public partial class CurrenciesViewModel : ObservableObject { - private ObservableCollection allCurrencies; + private ObservableCollection allCurrencies = new(); [ObservableProperty] - private ObservableCollection filteredCurrencies; + private ObservableCollection filteredCurrencies = new(); [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsCurrencySelected))] private CurrencyModel? selectedCurrency; [ObservableProperty] private string currencySearch = string.Empty; [ObservableProperty] - private string toCurrencyCode = string.Empty; + private bool isLoadingDetails = false; + + public bool IsCurrencySelected => SelectedCurrency != null; [ObservableProperty] - private string toCurrencyName = string.Empty; + private ISeries[] series = new ISeries[0]; [ObservableProperty] - private string toCurrencyPrice = string.Empty; + private Axis[] xAxes = new Axis[0]; - private readonly ILogger _logger; + [ObservableProperty] + private Axis[] yAxes = new Axis[0]; + + [ObservableProperty] + private LabelVisual title = new(); + + [ObservableProperty] + private ISeries[] gaugeSeries = new ISeries[0]; + + [ObservableProperty] + private IEnumerable> gaugeVisuals = new List>(); + + [ObservableProperty] + private ISeries[] pieSeries = new ISeries[0]; + + [ObservableProperty] + private string periodHighText = "N/A"; + + [ObservableProperty] + private string periodLowText = "N/A"; + + [ObservableProperty] + private string averageRateText = "N/A"; + + [ObservableProperty] + private double periodHighValue = 1; + + [ObservableProperty] + private double periodLowValue = 0; + private readonly ILogger _logger; private readonly IApiService _apiService; + private static readonly SKColor _chartColor = SKColor.Parse("#3498DB"); public CurrenciesViewModel(ILogger logger, IApiService apiService) { _logger = logger; - _apiService = apiService; - + InitializeEmptyChart(); _ = LoadCurrenciesData(); } + partial void OnCurrencySearchChanged(string value) { FilterCurrencies(); } - private void FilterCurrencies() + async partial void OnSelectedCurrencyChanged(CurrencyModel? value) { - if (string.IsNullOrWhiteSpace(CurrencySearch)) + if (value == null) { - FilteredCurrencies = new ObservableCollection(allCurrencies); + InitializeEmptyChart(); + return; } - else + await LoadHistoricalDataAsync(value.ToCurrencyCode); + } + + private async Task LoadCurrenciesData() + { + try { - var filtered = allCurrencies - .Where(c => c.ToCurrencyCode.Contains(CurrencySearch, StringComparison.OrdinalIgnoreCase) || - c.ToCurrencyName.Contains(CurrencySearch, StringComparison.OrdinalIgnoreCase)); - FilteredCurrencies = new ObservableCollection(filtered); + var response = await _apiService.GetAsync("Currency/latest/USD"); + if (response?.Rates != null) + { + allCurrencies.Clear(); + foreach (var item in response.Rates) + { + allCurrencies.Add(new CurrencyModel + { + Id = item.Id, + ToCurrencyCode = item.Code, + ToCurrencyName = item.CountryCode ?? "N/A", + ToCurrencyFlag = item.IconUrl ?? string.Empty, + ToCurrencyPrice = item.Rate + }); + } + FilterCurrencies(); + if (FilteredCurrencies.Any()) + { + SelectedCurrency = FilteredCurrencies.First(); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Para birimi listesi yüklenirken hata oluştu."); + InitializeEmptyChart("Failed to load currency list."); } + } - _logger.LogInformation("Para birimleri '{SearchText}' metnine göre filtrelendi.", CurrencySearch); + private async Task LoadHistoricalDataAsync(string targetCurrencyCode, string period = "1M") + { + IsLoadingDetails = true; + try + { + string baseCurrency = "USD"; + string endpoint = $"Currency/{baseCurrency}/history/{targetCurrencyCode}?period={period}"; + var historyData = await _apiService.GetAsync(endpoint); + + if (historyData == null || SelectedCurrency == null || !historyData.HistoricalRates.Any()) + { + InitializeEmptyChart($"No data available for {targetCurrencyCode}."); + UpdateDetails(null); + return; + } + + _logger.LogInformation("{Target} için geçmiş veriler yüklendi.", targetCurrencyCode); + var dailyHistoricalRates = UpdateChart(historyData); + UpdateAdditionalVisuals(dailyHistoricalRates); + UpdateDetails(historyData.ChangeSummary); + } + catch (Exception ex) + { + _logger.LogError(ex, "{Target} için geçmiş veriler yüklenirken hata oluştu.", targetCurrencyCode); + InitializeEmptyChart($"Error loading data for {targetCurrencyCode}."); + UpdateDetails(null); + } + finally + { + IsLoadingDetails = false; + } } - private async Task LoadCurrenciesData() + private List UpdateChart(CurrencyHistoryDto historyData) { - var currencies = await _apiService.GetAsync("Currency/latest/USD"); // TODO: USD yerine kullanıcının seçtiği para birimini kullan - allCurrencies = new ObservableCollection(); - if (currencies.Rates != null) + var dailyHistoricalRates = historyData.HistoricalRates + .GroupBy(r => r.Date.Date) + .Select(g => g.OrderBy(r => r.Date).Last()) + .Select(r => new HistoricalRatePointDto { Date = r.Date.Date, Rate = r.Rate }) + .ToList(); + + var historicalPoints = dailyHistoricalRates + .Select(p => new DateTimePoint(p.Date, (double)p.Rate)) + .OrderBy(p => p.DateTime) + .ToList(); + + Series = new ISeries[] + { + new LineSeries + { + Values = historicalPoints, + Name = $"{historyData.BaseCurrency}/{historyData.TargetCurrency}", + Stroke = new SolidColorPaint(_chartColor) { StrokeThickness = 3 }, + GeometrySize = 8, + GeometryStroke = new SolidColorPaint(_chartColor) { StrokeThickness = 3 }, + GeometryFill = new SolidColorPaint(SKColor.Parse("#1E222D")), + Fill = new LinearGradientPaint(_chartColor.WithAlpha(90), SKColors.Transparent, new SKPoint(0.5f, 0), new SKPoint(0.5f, 1)) + } + }; + + Title = new LabelVisual + { + Text = $"{historyData.TargetCurrency} Exchange Rate (Last 30 Days)", + TextSize = 16, + Padding = new Padding(15), + Paint = new SolidColorPaint(SKColors.WhiteSmoke) + }; + + XAxes = new Axis[] { - foreach (RateDetailDto item in currencies.Rates) + new Axis { - allCurrencies.Add(new CurrencyModel + Labeler = value => { - Id = item.Id, - ToCurrencyCode = item.Code, - ToCurrencyName = item.CountryCode ?? "N/A", - ToCurrencyFlag = item.IconUrl ?? "N/A", - ToCurrencyPrice = item.Rate, - ToCurrencyChange = "N/A", - Type = CurrencyConversionType.Increase, // TODO: Değişim bilgisi eklenmeli - DailyLow = "N/A", - DailyHigh = "N/A", - WeeklyChange = "N/A", - MonthlyChange = "N/A" - }); + try + { + var date = DateTime.FromOADate(value); + return date.ToString("dd MMM"); + } + catch + { + return string.Empty; + } + }, + LabelsPaint = new SolidColorPaint(SKColors.LightGray), + UnitWidth = TimeSpan.FromDays(1).Ticks, + MinStep = TimeSpan.FromDays(1).Ticks, + SeparatorsPaint = new SolidColorPaint(SKColors.Transparent) + } + }; + + YAxes = new Axis[] + { + new Axis + { + Labeler = value => value.ToString("N4", CultureInfo.InvariantCulture), + LabelsPaint = new SolidColorPaint(SKColors.LightGray), + SeparatorsPaint = new SolidColorPaint(SKColors.Gray) { StrokeThickness = 0.5f, PathEffect = new DashEffect(new float[] { 3, 3 }) } + } + }; + + return dailyHistoricalRates; + } + + private void UpdateAdditionalVisuals(List dailyRates) + { + if (dailyRates == null || !dailyRates.Any()) + { + ClearAdditionalVisuals(); + return; + } + + var rates = dailyRates.Select(r => r.Rate).ToList(); + var currentRate = (double)rates.Last(); + var periodHigh = (double)rates.Max(); + var periodLow = (double)rates.Min(); + var averageRate = (double)rates.Average(); + + PeriodHighText = periodHigh.ToString("N4"); + PeriodLowText = periodLow.ToString("N4"); + AverageRateText = averageRate.ToString("N4"); + + PeriodHighValue = periodHigh; + PeriodLowValue = periodLow; + + GaugeSeries = new ISeries[] + { + new PieSeries + { + Values = new double[] { currentRate }, + InnerRadius = 50, + Fill = new SolidColorPaint(_chartColor), + IsHoverable = false, + }, + new PieSeries + { + Values = new double[] { periodHigh - currentRate }, + InnerRadius = 50, + Fill = new SolidColorPaint(SKColors.DarkGray), + IsHoverable = false, } + }; + + GaugeVisuals = Enumerable.Empty>(); + + int upDays = 0; + int downDays = 0; + int noChangeDays = 0; + + for (int i = 1; i < dailyRates.Count; i++) + { + if (dailyRates[i].Rate > dailyRates[i - 1].Rate) upDays++; + else if (dailyRates[i].Rate < dailyRates[i - 1].Rate) downDays++; + else noChangeDays++; + } + + PieSeries = new ISeries[] + { + new PieSeries { Values = new [] { upDays }, Name = "Up Days", Fill = new SolidColorPaint(SKColor.Parse("#2ECC71")) }, + new PieSeries { Values = new [] { downDays }, Name = "Down Days", Fill = new SolidColorPaint(SKColor.Parse("#E74C3C")) }, + new PieSeries { Values = new [] { noChangeDays }, Name = "No Change", Fill = new SolidColorPaint(SKColors.Gray) } + }; + } + + private void ClearAdditionalVisuals() + { + GaugeSeries = new ISeries[0]; + GaugeVisuals = new List>(); + PieSeries = new ISeries[0]; + PeriodHighText = "N/A"; + PeriodLowText = "N/A"; + AverageRateText = "N/A"; + + PeriodHighValue = 1; + PeriodLowValue = 0; + } + + private void UpdateDetails(ChangeSummaryDto? summary) + { + if (SelectedCurrency == null) return; + + if (summary == null) + { + SelectedCurrency.DailyLow = "N/A"; + SelectedCurrency.DailyHigh = "N/A"; + SelectedCurrency.WeeklyChange = "N/A"; + SelectedCurrency.MonthlyChange = "N/A"; + SelectedCurrency.Type = CurrencyConversionType.Neutral; + SelectedCurrency.WeeklyChangeType = CurrencyConversionType.Neutral; + SelectedCurrency.MonthlyChangeType = CurrencyConversionType.Neutral; + return; + } + + SelectedCurrency.DailyLow = "N/A"; + SelectedCurrency.DailyHigh = "N/A"; + + (SelectedCurrency.WeeklyChange, SelectedCurrency.WeeklyChangeType) = FormatChangeValue(summary.WeeklyChangePercentage); + (SelectedCurrency.MonthlyChange, SelectedCurrency.MonthlyChangeType) = FormatChangeValue(summary.MonthlyChangePercentage); + (SelectedCurrency.ToCurrencyChange, SelectedCurrency.Type) = FormatChangeValue(summary.DailyChangePercentage); + } + + private void InitializeEmptyChart(string message = "Select a currency to view its data.") + { + Series = new ISeries[0]; + XAxes = new Axis[0]; + YAxes = new Axis[0]; + Title = new LabelVisual + { + Text = message, + TextSize = 18, + Paint = new SolidColorPaint(SKColors.Gray), + Padding = new Padding(15), + VerticalAlignment = Align.Middle + }; + ClearAdditionalVisuals(); + } + + private (string FormattedValue, CurrencyConversionType Type) FormatChangeValue(decimal? value, bool isPercentage = true, string format = "P2") + { + if (!value.HasValue) return ("N/A", CurrencyConversionType.Neutral); + + string formattedValue = value.Value.ToString(format, CultureInfo.InvariantCulture); + CurrencyConversionType type; + + if (value > 0) + { + formattedValue = "+" + formattedValue; + type = CurrencyConversionType.Increase; + } + else if (value < 0) + { + type = CurrencyConversionType.Decrease; + } + else + { + type = CurrencyConversionType.Neutral; + } + return (formattedValue, type); + } - FilterCurrencies(); - SelectedCurrency = FilteredCurrencies.FirstOrDefault(); - _logger.LogInformation("Para birimleri başarıyla yüklendi. Para birimler: {Currencies}", currencies); + private void FilterCurrencies() + { + if (string.IsNullOrWhiteSpace(CurrencySearch)) + { + FilteredCurrencies = new ObservableCollection(allCurrencies); + } + else + { + var searchText = CurrencySearch.ToLowerInvariant(); + var filtered = allCurrencies + .Where(c => c.ToCurrencyCode.ToLowerInvariant().Contains(searchText) || + c.ToCurrencyName.ToLowerInvariant().Contains(searchText)); + FilteredCurrencies = new ObservableCollection(filtered); } + _logger.LogInformation("Para birimleri '{SearchText}' metnine göre filtrelendi.", CurrencySearch); } } -} +} \ No newline at end of file diff --git a/FinTrack/ViewModels/DebtViewModel.cs b/FinTrack/ViewModels/DebtViewModel.cs index 8e5196c..b64861c 100644 --- a/FinTrack/ViewModels/DebtViewModel.cs +++ b/FinTrack/ViewModels/DebtViewModel.cs @@ -1,20 +1,28 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Core; +using FinTrackForWindows.Dtos.DebtDtos; using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Debt; +using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; +using Microsoft.Win32; using System.Collections.ObjectModel; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Windows; namespace FinTrackForWindows.ViewModels { public partial class DebtViewModel : ObservableObject { - private const string CurrentUserName = "Siz"; - private readonly ILogger _logger; + private readonly IApiService _apiService; + + private readonly int _currentUserId; - private ObservableCollection allDebts; + [ObservableProperty] + private bool isLoading; [ObservableProperty] private string? newProposalBorrowerEmail; @@ -22,6 +30,9 @@ public partial class DebtViewModel : ObservableObject [ObservableProperty] private decimal newProposalAmount; + [ObservableProperty] + private string newProposalDescription; + [ObservableProperty] private DateTime newProposalDueDate = DateTime.Now.AddMonths(1); @@ -31,139 +42,180 @@ public partial class DebtViewModel : ObservableObject [ObservableProperty] private ObservableCollection myDebtsList; - public DebtViewModel(ILogger logger) + public DebtViewModel(ILogger logger, IApiService apiService) { _logger = logger; - allDebts = new ObservableCollection(); + _apiService = apiService; + + var handler = new JwtSecurityTokenHandler(); + var jsonToken = handler.ReadJwtToken(SessionManager.CurrentToken); + _currentUserId = Convert.ToInt16(jsonToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value); + pendingOffers = new ObservableCollection(); myDebtsList = new ObservableCollection(); - LoadSampleData(); - RefreshLists(); + _ = LoadDebtsAsync(); } [RelayCommand] - private void SendOffer() + private async Task LoadDebtsAsync() { - if (string.IsNullOrWhiteSpace(NewProposalBorrowerEmail) || NewProposalAmount <= 0) + IsLoading = true; + _logger.LogInformation("Loading debt data from API..."); + try { - MessageBox.Show("Lütfen tüm alanları doldurun.", "Doğrulama Hatası"); - return; - } + var debtDtos = await _apiService.GetAsync>("Debt"); + if (debtDtos == null) return; - var newDebt = new DebtModel - { - LenderName = CurrentUserName, - BorrowerName = "Unknown", - Amount = NewProposalAmount, - DueDate = NewProposalDueDate, - Status = DebtStatusType.PendingBorrowerAcceptance, - CurrentUserName = CurrentUserName - }; + PendingOffers.Clear(); + MyDebtsList.Clear(); - allDebts.Add(newDebt); - RefreshLists(); - - NewProposalBorrowerEmail = string.Empty; - NewProposalAmount = 0; - _logger.LogInformation("Yeni borç teklifi gönderildi."); + foreach (var dto in debtDtos) + { + var debtModel = new DebtModel + { + Id = dto.Id, + LenderId = dto.LenderId, + BorrowerId = dto.BorrowerId, + CurrentUserId = _currentUserId, + LenderName = dto.LenderName, + BorrowerName = dto.BorrowerName, + borrowerImageUrl = dto.BorrowerProfilePicture ?? "/Assets/Images/Icons/user-red.png", + lenderImageUrl = dto.LenderProfilePicture ?? "/Assets/Images/Icons/user-green.png", + Amount = dto.Amount, + DueDate = dto.DueDateUtc.ToLocalTime(), + Status = dto.Status + }; + + // Bana gelen ve henüz karar vermediğim borç teklifleri + if (dto.Status == DebtStatusType.PendingBorrowerAcceptance && dto.BorrowerId == _currentUserId) + { + PendingOffers.Add(debtModel); + } + else // Diğer tüm borçlarım (aktif, ödenmiş, reddedilmiş vb.) + { + MyDebtsList.Add(debtModel); + } + } + _logger.LogInformation("Successfully loaded and processed {Count} debts.", debtDtos.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load debt data."); + MessageBox.Show("Borç verileri yüklenirken bir hata oluştu.", "Hata", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsLoading = false; + } } [RelayCommand] - private void ConfirmOffer(DebtModel debt) + private async Task SendOfferAsync() { - if (debt == null) return; + IsLoading = true; + try + { + if (string.IsNullOrWhiteSpace(NewProposalBorrowerEmail) || NewProposalAmount <= 0) + { + _logger.LogWarning("New proposal validation failed: Borrower email or amount is invalid."); + return; + } - debt.Status = DebtStatusType.PendingOperatorApproval; - debt.BorrowerName = CurrentUserName; - RefreshLists(); - _logger.LogInformation("{Amount} TRY tutarındaki borç teklifi onaylandı.", debt.Amount); - } + var createDto = new CreateDebtOfferRequestDto + { + BorrowerEmail = NewProposalBorrowerEmail, + Amount = NewProposalAmount, + CurrencyCode = BaseCurrencyType.TRY, + DueDateUtc = NewProposalDueDate, + Description = NewProposalDescription + }; - [RelayCommand] - private void RejectOffer(DebtModel debt) - { - if (debt == null) return; + _logger.LogInformation("Sending new debt offer to API..."); + var result = await _apiService.PostAsync("Debt/create-debt-offer", createDto); + + if (result != null) + { + NewProposalBorrowerEmail = string.Empty; + NewProposalAmount = 0; + await LoadDebtsAsync(); - debt.Status = DebtStatusType.RejectedByOperator; - RefreshLists(); - _logger.LogWarning("{Amount} TRY tutarındaki borç teklifi reddedildi.", debt.Amount); + _logger.LogInformation("Debt offer sent successfully."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send debt offer."); + } + finally + { + IsLoading = false; + } } [RelayCommand] - private void UploadVideo(DebtModel debt) + private async Task RespondToOfferAsync(object parameter) { - if (debt == null) return; + if (parameter is not object[] values || values.Length != 2 || values[0] is not DebtModel debt || values[1] is not bool decision) return; - debt.Status = DebtStatusType.PendingOperatorApproval; - RefreshLists(); - _logger.LogInformation("{Amount} TRY borcu için onay videosu yüklendi, operatör onayı bekleniyor.", debt.Amount); - MessageBox.Show("Video başarıyla yüklendi. Operatör onayı bekleniyor.", "Başarılı"); - } + IsLoading = true; + try + { + _logger.LogInformation("Attempting to {Action} offer for DebtId: {DebtId}", decision, debt.Id); - private void RefreshLists() - { - var pending = allDebts.Where(d => d.Status == DebtStatusType.PendingBorrowerAcceptance && d.LenderName != CurrentUserName).ToList(); - PendingOffers.Clear(); - foreach (var item in pending) PendingOffers.Add(item); + var requestBody = new { Accepted = decision }; + bool result = await _apiService.PostAsync($"Debt/respond-to-offer/{debt.Id}", requestBody); - var myDebts = allDebts.Where(d => d.Status != DebtStatusType.PendingBorrowerAcceptance && (d.LenderName == CurrentUserName || d.BorrowerName == CurrentUserName)).ToList(); - MyDebtsList.Clear(); - foreach (var item in myDebts) MyDebtsList.Add(item); + if (result) + { + _logger.LogInformation("Successfully responded to offer for DebtId: {DebtId}", debt.Id); + await LoadDebtsAsync(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to respond to debt offer for DebtId: {DebtId}", debt.Id); + } + finally + { + IsLoading = false; + } } - private void LoadSampleData() + [RelayCommand] + private async Task UploadVideoAsync(DebtModel debt) { - allDebts = new ObservableCollection + if (debt == null) return; + + OpenFileDialog openFileDialog = new OpenFileDialog { - new DebtModel - { - LenderName = "Ahmet Yılmaz", - BorrowerName = "Unknown", - Amount = 500, - DueDate = new DateTime(2024, 6, 15), - Status = DebtStatusType.RejectedByOperator, - CurrentUserName = CurrentUserName - }, - new DebtModel - { - LenderName = CurrentUserName, - BorrowerName = "Eylül Korkmaz", - Amount = 2500, - DueDate = new DateTime(2024, 10, 1), - Status = DebtStatusType.PendingOperatorApproval, - CurrentUserName = CurrentUserName - }, - new DebtModel + Filter = "Video Files (*.mp4;*.mov;*.wmv)|*.mp4;*.mov;*.wmv|All files (*.*)|*.*", + Title = "Select an Approval Video" + }; + + if (openFileDialog.ShowDialog() == true) + { + IsLoading = true; + try { - LenderName = "Sinem Berçem", - BorrowerName = CurrentUserName, - Amount = 30000, - DueDate = new DateTime(2024, 8, 31), - Status = DebtStatusType.Active, - CurrentUserName = CurrentUserName - }, - new DebtModel + _logger.LogInformation("Uploading video for DebtId: {DebtId}", debt.Id); + var success = await _apiService.UploadFileAsync($"Videos/user-upload-video?debtId={debt.Id}", openFileDialog.FileName); + + if (success) + { + _logger.LogInformation("Video uploaded successfully for DebtId: {DebtId}", debt.Id); + await LoadDebtsAsync(); + } + } + catch (Exception ex) { - LenderName = CurrentUserName, - BorrowerName = "Ali Veli", - Amount = 800000, - DueDate = new DateTime(2025, 1, 1), - Status = DebtStatusType.Active, - CurrentUserName = CurrentUserName - }, - new DebtModel + _logger.LogError(ex, "Failed to upload video for DebtId: {DebtId}", debt.Id); + } + finally { - LenderName = CurrentUserName, - BorrowerName = "Canan Aslan", - Amount = 1000, - DueDate = new DateTime(2024, 9, 20), - Status = DebtStatusType.RejectedByOperator, - RejectionReason = "Insufficient video", - CurrentUserName = CurrentUserName + IsLoading = false; } - }; - _logger.LogInformation("Örnek borç verileri yüklendi."); + } } } } \ No newline at end of file diff --git a/FinTrack/ViewModels/FeedbackViewModel.cs b/FinTrack/ViewModels/FeedbackViewModel.cs index c1a0cb8..68e1d03 100644 --- a/FinTrack/ViewModels/FeedbackViewModel.cs +++ b/FinTrack/ViewModels/FeedbackViewModel.cs @@ -1,6 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.FeedbackDtos; using FinTrackForWindows.Enums; +using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; using Microsoft.Win32; using System.Diagnostics; @@ -14,7 +16,7 @@ public partial class FeedbackViewModel : ObservableObject private string? inputSubject; [ObservableProperty] - private FeedbackTypes selectedFeedbackType; + private FeedbackType selectedFeedbackType; [ObservableProperty] private string? inputDescription; @@ -22,40 +24,55 @@ public partial class FeedbackViewModel : ObservableObject [ObservableProperty] private string? selectedFilePath; - public IEnumerable FeedbackTypes { get; } + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SendFeedbackCommand))] + private bool isSending = false; + + public IEnumerable FeedbackTypes { get; } private readonly ILogger _logger; - public FeedbackViewModel(ILogger logger) + private readonly IApiService _apiService; + + public FeedbackViewModel(ILogger logger, IApiService apiService) { _logger = logger; + _apiService = apiService; - FeedbackTypes = Enum.GetValues(typeof(FeedbackTypes)).Cast(); + FeedbackTypes = Enum.GetValues(typeof(FeedbackType)).Cast(); SelectedFeedbackType = FeedbackTypes.FirstOrDefault(); } [RelayCommand(CanExecute = nameof(CanSendFeedback))] - private void SendFeedback() + private async Task SendFeedback() { - string subject = InputSubject ?? "The title has been left blank."; - string description = InputDescription ?? "The description has been left blank."; - string filePath = SelectedFilePath ?? "No file selected."; - string feedbackType = SelectedFeedbackType.ToString(); + if (IsSending) return; + + var newFeedback = new FeedbackCreateDto + { + Subject = InputSubject ?? "The title has been left blank.", + Description = InputDescription ?? "The description has been left blank.", + SavedFilePath = SelectedFilePath ?? "No file selected.", + Type = SelectedFeedbackType, + }; - string feedbackMessage = $"Subject: {subject}\n" + - $"Type: {feedbackType}\n" + - $"Description: {description}\n" + - $"File Path: {filePath}"; + await _apiService.PostAsync("Feedback", newFeedback); - // TODO: feedbackMessage should be sent to a server or an email... + // TODO: Burada sisteme bir e-posta göndermekte fayda var... - MessageBox.Show(feedbackMessage, "Feedback Submitted", MessageBoxButton.OK, MessageBoxImage.Information); _logger.LogInformation("Feedback submitted: Subject: {Subject}, Type: {Type}, Description: {Description}, File Path: {FilePath}", - subject, feedbackType, description, filePath); + newFeedback.Subject, newFeedback.Type, newFeedback.Description, newFeedback.SavedFilePath); ClearForm(); } + private bool CanSendFeedback() + { + return !string.IsNullOrWhiteSpace(InputSubject) && + !string.IsNullOrWhiteSpace(InputDescription) && + !IsSending; + } + [RelayCommand] private void BrowseFile() { @@ -88,12 +105,6 @@ private void OpenLink(string? url) } } - private bool CanSendFeedback() - { - return !string.IsNullOrWhiteSpace(InputSubject) && - !string.IsNullOrWhiteSpace(InputDescription); - } - partial void OnInputSubjectChanged(string? value) => SendFeedbackCommand.NotifyCanExecuteChanged(); partial void OnInputDescriptionChanged(string? value) => SendFeedbackCommand.NotifyCanExecuteChanged(); diff --git a/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs b/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs index c0fc418..e8e32d5 100644 --- a/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs +++ b/FinTrack/ViewModels/NotificationSettingsContentViewModel.cs @@ -1,7 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.SettingsDtos; using FinTrackForWindows.Enums; using FinTrackForWindows.Models.Settings; +using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Windows; @@ -13,52 +15,103 @@ public partial class NotificationSettingsContentViewModel : ObservableObject public ObservableCollection EmailNotificationSettings { get; } [ObservableProperty] - private bool enableDesktopNotifications; + private bool _enableDesktopNotifications; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveChangesCommand))] + private bool _isSaving; private readonly ILogger _logger; + private readonly IApiService _apiService; - public NotificationSettingsContentViewModel(ILogger logger) + public NotificationSettingsContentViewModel(ILogger logger, IApiService apiService) { _logger = logger; - EnableDesktopNotifications = true; + _apiService = apiService; + EmailNotificationSettings = new ObservableCollection(); + _ = LoadSettings(); + } - LoadSettings(); + private async Task LoadSettings() + { + IsLoading = true; + try + { + var settingsDto = await _apiService.GetAsync("UserSettings/UserNotificationSettings"); + EmailNotificationSettings.Clear(); + + if (settingsDto != null) + { + // DTO'dan Model Listesine Mapping + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.SpendingLimitWarning, settingsDto.SpendingLimitWarning)); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.ExpectedBillReminder, settingsDto.ExpectedBillReminder)); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.WeeklySpendingSummary, settingsDto.WeeklySpendingSummary)); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.NewFeaturesAndAnnouncements, settingsDto.NewFeaturesAndAnnouncements)); + EnableDesktopNotifications = settingsDto.EnableDesktopNotifications; + } + else + { + // API'den veri gelmezse varsayılan değerlerle doldur + InitializeDefaultSettings(); + } + } + 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); + InitializeDefaultSettings(); + } + finally + { + IsLoading = false; + } } - private void LoadSettings() + [RelayCommand(CanExecute = nameof(CanSaveChanges))] + private async Task SaveChanges() { - foreach (NotificationSettingsType settingType in Enum.GetValues(typeof(NotificationSettingsType))) + IsSaving = true; + try { - bool isInitialSelected = settingType switch + var settingsUpdateDto = new UserNotificationSettingsUpdateDto { - NotificationSettingsType.SpendingLimitWarning => true, - NotificationSettingsType.ExpectedBillReminder => true, - NotificationSettingsType.WeeklySpendingSummary => false, - NotificationSettingsType.NewFeaturesAndAnnouncements => false, - _ => false + EnableDesktopNotifications = this.EnableDesktopNotifications, + SpendingLimitWarning = EmailNotificationSettings.FirstOrDefault(s => s.SettingType == NotificationSettingsType.SpendingLimitWarning)?.IsEnabled ?? false, + ExpectedBillReminder = EmailNotificationSettings.FirstOrDefault(s => s.SettingType == NotificationSettingsType.ExpectedBillReminder)?.IsEnabled ?? false, + WeeklySpendingSummary = EmailNotificationSettings.FirstOrDefault(s => s.SettingType == NotificationSettingsType.WeeklySpendingSummary)?.IsEnabled ?? false, + NewFeaturesAndAnnouncements = EmailNotificationSettings.FirstOrDefault(s => s.SettingType == NotificationSettingsType.NewFeaturesAndAnnouncements)?.IsEnabled ?? false }; - EmailNotificationSettings.Add(new NotificationSettingItemModel(settingType, isInitialSelected)); - } - EnableDesktopNotifications = true; + 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); + } + 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); + } + finally + { + IsSaving = false; + } } - [RelayCommand] - private void NotificationSettingsContentChanges() - { - var selectedSettings = EmailNotificationSettings - .Where(setting => setting.IsEnabled) - .Select(setting => setting.SettingType) - .ToList(); + private bool CanSaveChanges() => !IsSaving; - _logger.LogInformation("Kullanıcı bildirim ayarlarını değiştirdi: {SelectedSettings}", string.Join(", ", selectedSettings)); - MessageBox.Show( - "Bildirim ayarlarınız kaydedildi.", - "Bildirim Ayarları", - MessageBoxButton.OK, - MessageBoxImage.Information - ); + private void InitializeDefaultSettings() + { + EmailNotificationSettings.Clear(); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.SpendingLimitWarning, true)); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.ExpectedBillReminder, true)); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.WeeklySpendingSummary, false)); + EmailNotificationSettings.Add(new NotificationSettingItemModel(NotificationSettingsType.NewFeaturesAndAnnouncements, false)); + EnableDesktopNotifications = true; } } -} +} \ No newline at end of file diff --git a/FinTrack/ViewModels/NotificationViewModel.cs b/FinTrack/ViewModels/NotificationViewModel.cs index b532927..696a8c9 100644 --- a/FinTrack/ViewModels/NotificationViewModel.cs +++ b/FinTrack/ViewModels/NotificationViewModel.cs @@ -1,96 +1,150 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using FinTrackForWindows.Enums; +using FinTrackForWindows.Dtos.NotificationDtos; using FinTrackForWindows.Models.Notification; +using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; +using System.Windows; namespace FinTrackForWindows.ViewModels { public partial class NotificationViewModel : ObservableObject { [ObservableProperty] - private ObservableCollection notifications; + [NotifyPropertyChangedFor(nameof(IsListVisible))] + [NotifyPropertyChangedFor(nameof(ShowEmptyMessage))] + private ObservableCollection _notifications = new(); + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsListVisible))] + [NotifyPropertyChangedFor(nameof(ShowEmptyMessage))] + private bool _isLoading; + + public bool IsListVisible => !IsLoading && Notifications.Any(); + public bool ShowEmptyMessage => !IsLoading && !Notifications.Any(); private readonly ILogger _logger; + private readonly IApiService _apiService; - public NotificationViewModel(ILogger logger) + public NotificationViewModel(ILogger logger, IApiService apiService) { _logger = logger; - LoadSampleNotifications(); + _apiService = apiService; + + _ = LoadNotifications(); } - // TODO: [TEST] - private void LoadSampleNotifications() + [RelayCommand] + private async Task LoadNotifications() { - Notifications = new ObservableCollection + IsLoading = true; + try + { + var notificationDtos = await _apiService.GetAsync>("Notification"); + + if (notificationDtos != null) + { + var models = notificationDtos.Select(dto => new NotificationModel( + dto.Id, + dto.MessageHead, + dto.MessageBody, + dto.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm"), + dto.NotificationType, + dto.IsRead + )); + Notifications = new ObservableCollection(models); + } + } + catch (Exception ex) { - new NotificationModel - ( - "Yeni Bütçe Önerisi", - "Aylık 'Eğlence' harcamalarınız için yeni bir bütçe limiti önerimiz var. Göz atmak için tıklayın.", - "2 saat önce", - NotificationType.Suggestion, - false - ), - new NotificationModel - ( - "Hedefe Ulaşıldı!", - "'Yeni Bilgisayar' birikim hedefinize ulaştınız. Tebrikler!", - "1 gün önce", - NotificationType.GoalAchieved, - true - ), - new NotificationModel - ( - "Fatura Hatırlatması", - "İnternet faturanızın son ödeme tarihi yarın. Gecikme faizinden kaçınmak için ödeme yapın.", - "3 gün önce", - NotificationType.Warning, - false - ) - }; + _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); + Notifications.Clear(); + } + finally + { + IsLoading = false; + } } [RelayCommand] - private void MarkAllAsRead() + private async Task MarkAllAsRead() { - foreach (var notification in Notifications) + if (!Notifications.Any(n => n.IsUnread)) return; + + try { - if (notification.IsUnread) + await _apiService.PostAsync("Notification/mark-all-as-read", null); + + foreach (var notification in Notifications) { - notification.IsRead = true; - _logger.LogInformation($"Notification '{notification.Title}' marked as read."); + if (notification.IsUnread) + { + notification.IsRead = true; + } } + _logger.LogInformation("All notifications marked as read on server and client."); + } + 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); } - _logger.LogInformation("All notifications marked as read."); } [RelayCommand] - private void ClearAll() + private async Task ClearAll() { - Notifications.Clear(); - _logger.LogInformation("All notifications cleared."); + if (!Notifications.Any()) return; + + try + { + await _apiService.DeleteAsync("Notification/clear-all"); + Notifications.Clear(); + _logger.LogInformation("All notifications cleared from server and client."); + } + 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); + } } [RelayCommand] - private void MarkAsRead(NotificationModel? notification) + private async Task MarkAsRead(NotificationModel? notification) { - if (notification.IsUnread) + if (notification == null || notification.IsRead) return; + + try { + await _apiService.PostAsync($"Notification/mark-as-read/{notification.Id}", null); notification.IsRead = true; - _logger.LogInformation($"Notification '{notification.Title}' marked as read."); + _logger.LogInformation($"Notification '{notification.Title}' marked as read on server and client."); + } + 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); } } [RelayCommand] - private void DeleteNotification(NotificationModel? notification) + private async Task DeleteNotification(NotificationModel? notification) { - if (notification != null) + if (notification == null) return; + + try { + await _apiService.DeleteAsync($"Notification/{notification.Id}"); Notifications.Remove(notification); - _logger.LogInformation($"Notification '{notification.Title}' deleted."); + _logger.LogInformation($"Notification '{notification.Title}' deleted from server and client."); + } + 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); } } } -} +} \ No newline at end of file diff --git a/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs b/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs index 9f9bd7f..3624a97 100644 --- a/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs +++ b/FinTrack/ViewModels/ProfileSettingsContentViewModel.cs @@ -1,5 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FinTrackForWindows.Dtos.SettingsDtos; +using FinTrackForWindows.Services.Api; using Microsoft.Extensions.Logging; using System.Windows; @@ -18,22 +20,35 @@ public partial class ProfileSettingsContentViewModel : ObservableObject private readonly ILogger _logger; - public ProfileSettingsContentViewModel(ILogger logger) + private readonly IApiService _apiService; + + public ProfileSettingsContentViewModel(ILogger logger, IApiService apiService) { _logger = logger; - LoadProfileData(); + _apiService = apiService; + _ = LoadProfileData(); } - private void LoadProfileData() + private async Task LoadProfileData() { - FullName = "John Doe"; - Email = "johndoe@gmail.com"; - ProfilePhotoUrl = "https://example.com/profile-photo.jpg"; + var profile = await _apiService.GetAsync("UserSettings/ProfileSettings"); + if (profile != null) + { + FullName = profile.FullName; + Email = profile.Email; + ProfilePhotoUrl = profile.ProfilePictureUrl ?? "N/A"; + } } [RelayCommand] - private void ProfileSettingsContentSaveChanges() + private async Task ProfileSettingsContentSaveChanges() { + await _apiService.PostAsync("UserSettings/ProfileSettings", new ProfileSettingsUpdateDto + { + FullName = FullName, + 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); } diff --git a/FinTrack/ViewModels/ReportsViewModel.cs b/FinTrack/ViewModels/ReportsViewModel.cs index 9931ed0..87a0fd1 100644 --- a/FinTrack/ViewModels/ReportsViewModel.cs +++ b/FinTrack/ViewModels/ReportsViewModel.cs @@ -1,7 +1,13 @@ -using CommunityToolkit.Mvvm.ComponentModel; +// ViewModels/ReportsViewModel.cs +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 Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Text; @@ -9,25 +15,36 @@ namespace FinTrackForWindows.ViewModels { + // partial class olduğundan emin olun public partial class ReportsViewModel : ObservableObject { + private readonly ILogger _logger; + private readonly IApiService _apiService; + public ObservableCollection AvailableReportTypes { get; } public ObservableCollection AvailableAccounts { get; } public ObservableCollection AvailableCategories { get; } public ObservableCollection SortingCriteria { get; } - public ObservableCollection AvailableExportFormats { get; } + public ObservableCollection AvailableDocumentFormats { get; } + // --- DOĞRU KULLANIM: Sadece [ObservableProperty] ile private alanları tanımlayın --- [ObservableProperty] [NotifyPropertyChangedFor(nameof(ReportSummary))] private ReportType selectedReportType; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ReportSummary))] - private DateTime startDate = new DateTime(DateTime.Now.Year, 1, 1); + private DateTime? startDate = new DateTime(DateTime.Now.Year, 1, 1); [ObservableProperty] [NotifyPropertyChangedFor(nameof(ReportSummary))] - private DateTime endDate = new DateTime(DateTime.Now.Year, 12, 31); + private DateTime? endDate = new DateTime(DateTime.Now.Year, 12, 31); + + [ObservableProperty] + private decimal? minBalance; + + [ObservableProperty] + private decimal? maxBalance; [ObservableProperty] private bool isIncomeSelected = true; @@ -39,76 +56,147 @@ public partial class ReportsViewModel : ObservableObject private string selectedSortingCriterion; [ObservableProperty] - private ExportFormat selectedExportFormat; + private DocumentFormat selectedDocumentFormat; - private readonly ILogger _logger; + [ObservableProperty] + private bool isBusy; public string ReportSummary { get { var summary = new StringBuilder(); - if (SelectedReportType != null) - { - summary.Append($"{SelectedReportType} Report, "); - summary.Append($"{StartDate:dd.MM.yyyy} - {EndDate:dd.MM.yyyy} "); - summary.Append("it will include all accounts and categories between the dates."); - } + summary.Append($"{SelectedReportType} Report, "); + summary.Append($"{StartDate:dd.MM.yyyy} - {EndDate:dd.MM.yyyy}. "); + summary.Append("This report will be created based on the selected filters."); return summary.ToString(); } } - public ReportsViewModel(ILogger logger) + public ReportsViewModel(ILogger logger, IApiService apiService) { _logger = logger; + _apiService = apiService; AvailableReportTypes = new ObservableCollection(Enum.GetValues(typeof(ReportType)).Cast()); AvailableAccounts = new ObservableCollection(); AvailableCategories = new ObservableCollection(); SortingCriteria = new ObservableCollection(); - AvailableExportFormats = new ObservableCollection(Enum.GetValues(typeof(ExportFormat)).Cast()); + AvailableDocumentFormats = new ObservableCollection(Enum.GetValues(typeof(DocumentFormat)).Cast()); - LoadSampleData(); + _ = LoadInitialDataAsync(); } - private void LoadSampleData() + private async Task LoadInitialDataAsync() { - AvailableAccounts.Clear(); - AvailableAccounts.Add(new SelectableOptionReport("All Accounts", true)); - AvailableAccounts.Add(new SelectableOptionReport("Cash")); - AvailableAccounts.Add(new SelectableOptionReport("Bank Account - A")); - AvailableAccounts.Add(new SelectableOptionReport("Credit Card - B")); - - AvailableCategories.Clear(); - AvailableCategories.Add(new SelectableOptionReport("All Categories", true)); - AvailableCategories.Add(new SelectableOptionReport("Groceries")); - AvailableCategories.Add(new SelectableOptionReport("Salary")); - AvailableCategories.Add(new SelectableOptionReport("Rent")); - - 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)"); - SelectedReportType = AvailableReportTypes.FirstOrDefault(); - SelectedSortingCriterion = SortingCriteria.FirstOrDefault(); - SelectedExportFormat = AvailableExportFormats.FirstOrDefault(); + 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)"); + + SelectedReportType = AvailableReportTypes.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"); + } + finally + { + IsBusy = false; + } } [RelayCommand] - private void SelectExportFormat(ExportFormat format) + private void SelectDocumentFormat(DocumentFormat format) { - SelectedExportFormat = format; - _logger?.LogInformation("Selected export format: {ExportFormat}", format); + SelectedDocumentFormat = format; } [RelayCommand] - private void CreateReport() + private async Task CreateReport() { - _logger?.LogInformation("Aşağıdaki parametrelerle rapor oluşturma: {ReportType}, {StartDate} ila {EndDate}, Seçilen Gelir: {IsIncomeSelected}, Seçilen Gider: {IsExpenseSelected}, Sıralama Kriterleri: {SortingCriteria}, Dışa Aktarma Biçimi: {ExportFormat}", - SelectedReportType, StartDate, EndDate, IsIncomeSelected, IsExpenseSelected, SelectedSortingCriterion, SelectedExportFormat); + if (IsBusy) return; - MessageBox.Show("Report creation logic triggered! Check your logs.", "Success"); + IsBusy = true; + try + { + var reportRequest = new ReportRequestDto + { + ReportType = SelectedReportType, + ExportFormat = SelectedDocumentFormat, + StartDate = StartDate, + EndDate = EndDate, + MinBalance = MinBalance, + MaxBalance = MaxBalance, + IsIncomeSelected = IsIncomeSelected, + IsExpenseSelected = IsExpenseSelected, + SelectedSortingCriterion = SelectedSortingCriterion, + SelectedAccountIds = AvailableAccounts + .Where(acc => acc.IsSelected) + .Select(acc => acc.Id) + .ToList(), + SelectedCategoryIds = AvailableCategories + .Where(cat => cat.IsSelected) + .Select(cat => cat.Id) + .ToList() + }; + + _logger.LogInformation("Sending report creation request. Type: {ReportType}", reportRequest.ReportType); + + var result = await _apiService.PostAndDownloadReportAsync("Reports/generate", reportRequest); + + 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); + MessageBox.Show($"Report created successfully and saved to '{savedPath}'.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + 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); + } + } + 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); + } + finally + { + IsBusy = false; + } } } } \ No newline at end of file diff --git a/FinTrack/Views/AccountView.xaml b/FinTrack/Views/AccountView.xaml index aa23a36..334e543 100644 --- a/FinTrack/Views/AccountView.xaml +++ b/FinTrack/Views/AccountView.xaml @@ -72,16 +72,16 @@ + Height="280" + Series="{Binding Series}" + XAxes="{Binding XAxes}" + YAxes="{Binding YAxes}" + Title="{Binding Title}" + LegendPosition="Bottom" + LegendTextPaint="{Binding Source={StaticResource TopBarTextSecondaryBrush}, Converter={StaticResource BrushToLvcPaintConverter}}" + TooltipPosition="Top" + TooltipBackgroundPaint="{Binding Source={StaticResource CardDarkerBackgroundBrush}, Converter={StaticResource BrushToLvcPaintConverter}}" + TooltipTextPaint="{Binding Source={StaticResource TopBarTextSecondaryBrush}, Converter={StaticResource BrushToLvcPaintConverter}}"> diff --git a/FinTrack/Views/AppSettingsContentView.xaml b/FinTrack/Views/AppSettingsContentView.xaml index 6e5fa40..64ff1e6 100644 --- a/FinTrack/Views/AppSettingsContentView.xaml +++ b/FinTrack/Views/AppSettingsContentView.xaml @@ -19,8 +19,8 @@ Margin="0,5,0,20" Opacity="0.5"/> - + @@ -29,8 +29,8 @@ - + @@ -39,14 +39,15 @@ - + + - - - - - - - - + + + + + + + @@ -111,18 +137,14 @@ -