Added Budgets Create/Update/Delete, Account Create/Update/Delete, and fully added settings tab, and refactored a lot of the data logic
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ obj/
|
||||
.vs/
|
||||
.idea/
|
||||
*.user
|
||||
*.suo
|
||||
*.suo
|
||||
./Clario/CLAUDE_CONTEXT.md
|
||||
@@ -15,10 +15,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Android"/>
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" />
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
<PackageReference Include="Supabase" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen"/>
|
||||
|
||||
@@ -8,15 +8,18 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Browser"/>
|
||||
<PackageReference Include="Avalonia.Svg.Skia" />
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
<PackageReference Include="Supabase" />
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia"/>
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing"/>
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
|
||||
<PackageReference Include="SkiaSharp"/>
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>
|
||||
<PackageReference Include="Supabase"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Clario\Clario.csproj" />
|
||||
<ProjectReference Include="..\Clario\Clario.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -10,6 +11,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" />
|
||||
<PackageReference Include="Avalonia.Desktop"/>
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Include="Avalonia.Diagnostics">
|
||||
@@ -20,6 +22,7 @@
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
<PackageReference Include="Supabase" />
|
||||
</ItemGroup>
|
||||
|
||||
36
Clario.Desktop/Clario.Desktop.parcel
Normal file
36
Clario.Desktop/Clario.Desktop.parcel
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"GeneralSettings": {
|
||||
"NetProjectPath": "Clario.Desktop.csproj",
|
||||
"ApplicationName": "Clario",
|
||||
"Version": "0.3.0",
|
||||
"PackageName": {
|
||||
"$type": "msbuild",
|
||||
"property": "AssemblyName"
|
||||
},
|
||||
"AssemblyName": {
|
||||
"$type": "msbuild",
|
||||
"property": "AssemblyName"
|
||||
}
|
||||
},
|
||||
"LinuxSettings": {
|
||||
"AppIcon": "../Clario/Assets/Logo.png",
|
||||
"CreateBinSymlink": "True"
|
||||
},
|
||||
"Win32Settings": {
|
||||
"InstallerIcon": "../Clario/Assets/Clario-Logo.svg",
|
||||
"Company": "Clario",
|
||||
"IncludeUninstaller": "True"
|
||||
},
|
||||
"MacOsSettings": {
|
||||
"CreateBundle": true,
|
||||
"BundleIdentifier": "com.CompanyName.Clario-Desktop",
|
||||
"SigningCredentialsType": "AdHoc"
|
||||
},
|
||||
"PublishSettings": {
|
||||
"PublishSingleFile": "True",
|
||||
"PublishReadyToRun": "True",
|
||||
"ExtraBuildProperties": {
|
||||
"RuntimeFrameworkVersion": "8.0.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" />
|
||||
<PackageReference Include="Avalonia.iOS"/>
|
||||
<PackageReference Include="Avalonia.Svg.Skia" />
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
<PackageReference Include="Supabase" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -28,10 +28,13 @@
|
||||
<converters:DecimalColorConverter x:Key="DecimalColorConverter" />
|
||||
<converters:BoolToColorConverter x:Key="BoolToColorConverter" />
|
||||
<converters:BoolToCssConverter x:Key="BoolToCssConverter" />
|
||||
<converters:CreditAmountConverter x:Key="CreditAmountConverter"/>
|
||||
<converters:BoolToStringConverter x:Key="BoolToStringConverter"/>
|
||||
</Application.Resources>
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="../Theme/AppTheme.axaml" />
|
||||
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -50,7 +50,7 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
IsMobile = ApplicationLifetime is ISingleViewApplicationLifetime;
|
||||
|
||||
|
||||
var culture = new CultureInfo("en-US");
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
@@ -60,8 +60,9 @@ public partial class App : Application
|
||||
{
|
||||
await SupabaseService.Client.Auth.RetrieveSessionAsync();
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"[Auth] RetrieveSession failed: {e.Message}");
|
||||
}
|
||||
|
||||
var user = SupabaseService.Client.Auth.CurrentUser;
|
||||
|
||||
1
Clario/Assets/Icons/circle-check.svg
Normal file
1
Clario/Assets/Icons/circle-check.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
1
Clario/Assets/Icons/log-out.svg
Normal file
1
Clario/Assets/Icons/log-out.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-log-out-icon lucide-log-out"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
1
Clario/Assets/Icons/refresh-cw.svg
Normal file
1
Clario/Assets/Icons/refresh-cw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw-icon lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
||||
|
After Width: | Height: | Size: 411 B |
1
Clario/Assets/Icons/upload.svg
Normal file
1
Clario/Assets/Icons/upload.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-upload-icon lucide-upload"><path d="M12 3v12"/><path d="m17 8-5-5-5 5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>
|
||||
|
After Width: | Height: | Size: 333 B |
1
Clario/Assets/Icons/wallet-cards.svg
Normal file
1
Clario/Assets/Icons/wallet-cards.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wallet-cards-icon lucide-wallet-cards"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"/><path d="M3 11h3c.8 0 1.6.3 2.1.9l1.1.9c1.6 1.6 4.1 1.6 5.7 0l1.1-.9c.5-.5 1.3-.9 2.1-.9H21"/></svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia"/>
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent"/>
|
||||
<PackageReference Include="Avalonia.Fonts.Inter"/>
|
||||
@@ -30,6 +31,7 @@
|
||||
<PackageReference Include="Xaml.Behaviors.Interactions" />
|
||||
<PackageReference Include="Xaml.Behaviors.Interactivity" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -38,5 +40,8 @@
|
||||
<DependentUpon>MobileMainView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\AccountFormView.axaml.cs">
|
||||
<DependentUpon>AccountFormView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
20
Clario/Converters/BoolToStringConverter.cs
Normal file
20
Clario/Converters/BoolToStringConverter.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Clario.Converters;
|
||||
|
||||
public class BoolToStringConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not bool b || parameter is not string s) return null;
|
||||
var results = s.Split('|');
|
||||
return b ? results[0] : results[1];
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
19
Clario/Converters/CreditAmountConverter.cs
Normal file
19
Clario/Converters/CreditAmountConverter.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Clario.Converters;
|
||||
|
||||
public class CreditAmountConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not decimal amount) return 0;
|
||||
return amount < 0 ? amount * -1 : 0;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Controls.Converters;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
@@ -21,6 +22,18 @@ public class HexToColorConverter : IValueConverter
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (parameter is not string type) return null;
|
||||
var color = Color.Parse("#ffffff");
|
||||
if (value is Color c)
|
||||
{
|
||||
color = c;
|
||||
}
|
||||
|
||||
if (value is SolidColorBrush b)
|
||||
{
|
||||
color = b.Color;
|
||||
}
|
||||
|
||||
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,76 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Clario.CustomControls">
|
||||
|
||||
<Styles.Resources>
|
||||
<!--
|
||||
Full ControlTheme replacement for CalendarButton.
|
||||
This is necessary because in Avalonia 11 the Fluent theme owns
|
||||
the internal template parts at ControlTheme priority, making
|
||||
external /template/ style selectors unreliable for CalendarButton.
|
||||
Replacing the entire ControlTheme is the only reliable approach.
|
||||
-->
|
||||
<ControlTheme x:Key="{x:Type CalendarButton}" TargetType="CalendarButton">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="6"
|
||||
Padding="4">
|
||||
<ContentControl x:Name="Content"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<!-- Hover -->
|
||||
<Style Selector="^:pointerover /template/ Border#Root">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover /template/ ContentControl#Content">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
|
||||
</Style>
|
||||
|
||||
<!-- Pressed -->
|
||||
<Style Selector="^:pressed /template/ Border#Root">
|
||||
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
|
||||
</Style>
|
||||
|
||||
<!-- Selected (current month/year) -->
|
||||
<Style Selector="^:selected /template/ Border#Root">
|
||||
<Setter Property="Background" Value="{DynamicResource BorderAccent}" />
|
||||
</Style>
|
||||
<Style Selector="^:selected /template/ ContentControl#Content">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
|
||||
</Style>
|
||||
|
||||
<!-- Selected + hover -->
|
||||
<Style Selector="^:selected:pointerover /template/ Border#Root">
|
||||
<Setter Property="Background" Value="{DynamicResource BorderAccent}" />
|
||||
</Style>
|
||||
|
||||
<!-- Inactive (out-of-range months/years) -->
|
||||
<Style Selector="^:inactive /template/ ContentControl#Content">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuted}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</Styles.Resources>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- DateRangePicker control template -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<Style Selector="local|DateRangePicker">
|
||||
<Setter Property="MinHeight" Value="15" />
|
||||
<Setter Property="MinWidth" Value="50" />
|
||||
@@ -14,8 +84,6 @@
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid>
|
||||
|
||||
<!-- Trigger button -->
|
||||
<Button x:Name="PART_Button"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
@@ -48,7 +116,6 @@
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Popup -->
|
||||
<Popup x:Name="PART_Popup"
|
||||
PlacementTarget="{Binding #PART_Button}"
|
||||
Placement="Bottom"
|
||||
@@ -65,21 +132,50 @@
|
||||
BorderThickness="0" />
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- pointerover -->
|
||||
<Style Selector="local|DateRangePicker:pointerover /template/ Button#PART_Button">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}" />
|
||||
</Style>
|
||||
|
||||
<!-- pressed -->
|
||||
<Style Selector="local|DateRangePicker:pressed /template/ Button#PART_Button">
|
||||
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
<!-- ============================================================ -->
|
||||
<!-- CalendarItem: nav header buttons (prev / title / next) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<Style Selector="CalendarItem /template/ Button#PART_HeaderButton">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
<Style Selector="CalendarItem /template/ Button#PART_HeaderButton:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="CalendarItem /template/ Button#PART_PreviousButton">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}" />
|
||||
</Style>
|
||||
<Style Selector="CalendarItem /template/ Button#PART_PreviousButton:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="CalendarItem /template/ Button#PART_NextButton">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}" />
|
||||
</Style>
|
||||
<Style Selector="CalendarItem /template/ Button#PART_NextButton:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
|
||||
@@ -5,8 +5,10 @@ using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using Calendar = Avalonia.Controls.Calendar;
|
||||
|
||||
namespace Clario.CustomControls;
|
||||
@@ -23,19 +25,28 @@ public class DateRangePicker : TemplatedControl
|
||||
set => SetValue(SelectionModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IList<DateTime>> SelectedDatesProperty =
|
||||
AvaloniaProperty.Register<DateRangePicker, IList<DateTime>>(
|
||||
nameof(SelectedDates), new List<DateTime>());
|
||||
// FIX: Use DirectProperty to avoid shared-instance default and get proper TwoWay support
|
||||
private IList<DateTime> _selectedDates = new List<DateTime>();
|
||||
|
||||
public static readonly DirectProperty<DateRangePicker, IList<DateTime>> SelectedDatesProperty =
|
||||
AvaloniaProperty.RegisterDirect<DateRangePicker, IList<DateTime>>(
|
||||
nameof(SelectedDates),
|
||||
o => o.SelectedDates,
|
||||
(o, v) => o.SelectedDates = v,
|
||||
defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public IList<DateTime> SelectedDates
|
||||
{
|
||||
get => GetValue(SelectedDatesProperty);
|
||||
set => SetValue(SelectedDatesProperty, value);
|
||||
get => _selectedDates;
|
||||
set => SetAndRaise(SelectedDatesProperty, ref _selectedDates, value);
|
||||
}
|
||||
|
||||
// FIX: Add defaultBindingMode: TwoWay so changes propagate back to the ViewModel
|
||||
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
|
||||
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
|
||||
nameof(SelectedDate), null);
|
||||
nameof(SelectedDate),
|
||||
defaultValue: null,
|
||||
defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public DateTime? SelectedDate
|
||||
{
|
||||
@@ -58,7 +69,6 @@ public class DateRangePicker : TemplatedControl
|
||||
private Popup? _popup;
|
||||
private Calendar? _calendar;
|
||||
|
||||
|
||||
private bool _isSyncing = false;
|
||||
|
||||
|
||||
@@ -66,12 +76,12 @@ public class DateRangePicker : TemplatedControl
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
|
||||
if (_button != null) _button.Click -= OnButtonClick;
|
||||
if (_calendar != null)
|
||||
{
|
||||
_calendar.SelectedDatesChanged -= OnCalendarDatesChanged;
|
||||
_calendar.RemoveHandler(PointerReleasedEvent, OnCalendarPointerReleased);
|
||||
// _calendar.RemoveHandler(Button.ClickEvent, OnCalendarInternalClick); // add this
|
||||
}
|
||||
|
||||
_button = e.NameScope.Find<Button>("PART_Button");
|
||||
@@ -85,10 +95,9 @@ public class DateRangePicker : TemplatedControl
|
||||
{
|
||||
_calendar.AllowTapRangeSelection = true;
|
||||
|
||||
|
||||
_calendar.SelectedDatesChanged += OnCalendarDatesChanged;
|
||||
_calendar.AddHandler(PointerReleasedEvent, OnCalendarPointerReleased, RoutingStrategies.Tunnel);
|
||||
|
||||
// _calendar.AddHandler(Button.ClickEvent, OnCalendarInternalClick, RoutingStrategies.Tunnel);
|
||||
|
||||
SyncToCalendar();
|
||||
}
|
||||
@@ -96,27 +105,32 @@ public class DateRangePicker : TemplatedControl
|
||||
UpdateDisplayText();
|
||||
}
|
||||
|
||||
private void OnCalendarInternalClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCalendarPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (_calendar!.SelectionMode != CalendarSelectionMode.SingleDate) return;
|
||||
|
||||
if (_isSyncing) return;
|
||||
|
||||
if (_popup is null || !_popup.IsOpen) return;
|
||||
|
||||
// FIX: Ignore clicks on the nav buttons/header — only react to day cell clicks
|
||||
if (e.Source is not Control source) return;
|
||||
if (source.TemplatedParent is CalendarDayButton == false &&
|
||||
source.FindAncestorOfType<CalendarDayButton>() is null)
|
||||
return;
|
||||
|
||||
var newDates = _calendar!.SelectedDates.OrderBy(d => d).ToList();
|
||||
|
||||
_isSyncing = true;
|
||||
try
|
||||
{
|
||||
SelectedDates = newDates;
|
||||
|
||||
|
||||
SelectedDate = newDates.Count > 0 ? newDates[0] : null;
|
||||
|
||||
UpdateDisplayText();
|
||||
|
||||
|
||||
bool shouldClose = SelectionMode switch
|
||||
{
|
||||
CalendarSelectionMode.SingleDate => newDates.Count >= 1,
|
||||
@@ -133,12 +147,10 @@ public class DateRangePicker : TemplatedControl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_popup is null) return;
|
||||
|
||||
|
||||
SyncToCalendar();
|
||||
_popup.IsOpen = true;
|
||||
}
|
||||
@@ -157,13 +169,10 @@ public class DateRangePicker : TemplatedControl
|
||||
try
|
||||
{
|
||||
SelectedDates = newDates;
|
||||
|
||||
|
||||
SelectedDate = newDates.Count > 0 ? newDates[0] : null;
|
||||
|
||||
UpdateDisplayText();
|
||||
|
||||
|
||||
bool shouldClose = SelectionMode switch
|
||||
{
|
||||
CalendarSelectionMode.SingleDate => newDates.Count >= 1,
|
||||
@@ -180,7 +189,6 @@ public class DateRangePicker : TemplatedControl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
@@ -220,7 +228,6 @@ public class DateRangePicker : TemplatedControl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void SyncToCalendar()
|
||||
{
|
||||
if (_calendar is null || _isSyncing) return;
|
||||
|
||||
@@ -1,49 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Clario.Models;
|
||||
using Clario.Models.GeneralModels;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Supabase.Postgrest;
|
||||
using FileOptions = Supabase.Storage.FileOptions;
|
||||
|
||||
namespace Clario.Data;
|
||||
|
||||
public class GeneralDataRepo
|
||||
public record ProfileUpdated();
|
||||
|
||||
public partial class GeneralDataRepo : ObservableObject
|
||||
{
|
||||
public Profile? Profile { get; set; }
|
||||
public List<Category>? Categories { get; set; }
|
||||
public List<Account>? Accounts { get; set; }
|
||||
public List<Budget>? Budgets { get; set; }
|
||||
public List<Transaction>? Transactions { get; set; }
|
||||
[ObservableProperty] private Profile? _profile;
|
||||
[ObservableProperty] private ObservableCollection<Category> _categories = new();
|
||||
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
|
||||
[ObservableProperty] private ObservableCollection<Budget> _budgets = new();
|
||||
[ObservableProperty] private ObservableCollection<Transaction> _transactions = new();
|
||||
|
||||
public async Task<Profile?> FetchProfileInfo()
|
||||
private static readonly HttpClient _HttpClient = new();
|
||||
private const string Bucket = "avatars";
|
||||
private const string ProjectRef = "xzxstbllaivumhtpctmo";
|
||||
private const string PublicBaseUrl = $"https://{ProjectRef}.supabase.co/storage/v1/object/public/{Bucket}";
|
||||
|
||||
partial void OnProfileChanged(Profile? value)
|
||||
{
|
||||
if (Profile is not null) return Profile;
|
||||
_ = GetAvatarFromUrl(value?.AvatarUrl);
|
||||
}
|
||||
|
||||
public async Task<Profile?> FetchProfileInfo(bool forceRefresh = false)
|
||||
{
|
||||
if (Profile is not null && !forceRefresh) return Profile;
|
||||
var profile = await SupabaseService.Client.From<Profile>().Get();
|
||||
if (profile.Models.Count == 0) return null;
|
||||
Profile = profile.Model;
|
||||
return profile.Model;
|
||||
|
||||
return Profile;
|
||||
}
|
||||
|
||||
public async Task InsertProfileInfo(Profile profile)
|
||||
private async Task GetAvatarFromUrl(string? url)
|
||||
{
|
||||
try
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
await SupabaseService.Client.From<Profile>().Insert(profile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return;
|
||||
var bytes = await _HttpClient.GetByteArrayAsync(url);
|
||||
var stream = new MemoryStream(bytes);
|
||||
Profile.Avatar = new Bitmap(stream);
|
||||
}
|
||||
|
||||
Profile = profile;
|
||||
WeakReferenceMessenger.Default.Send(new ProfileUpdated());
|
||||
}
|
||||
|
||||
public async Task<List<Transaction>> FetchTransactions()
|
||||
|
||||
public async Task<List<Transaction>> FetchTransactions(bool forceRefresh = false)
|
||||
{
|
||||
if (Transactions is not null) return Transactions;
|
||||
if (Transactions.Count != 0 && !forceRefresh) return Transactions.ToList();
|
||||
var transactions = await SupabaseService.Client.From<Transaction>().Get();
|
||||
Transactions = transactions.Models;
|
||||
Transactions = new ObservableCollection<Transaction>(transactions.Models);
|
||||
return transactions.Models;
|
||||
}
|
||||
|
||||
@@ -51,11 +73,18 @@ public class GeneralDataRepo
|
||||
{
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.From<Transaction>().Insert(transaction);
|
||||
var result = await SupabaseService.Client.From<Transaction>().Insert(transaction);
|
||||
|
||||
if (result.Models.Count >= 1)
|
||||
{
|
||||
var resultItem = LinkTransactionCategories(result.Models[0]);
|
||||
Transactions.Add(resultItem);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +92,13 @@ public class GeneralDataRepo
|
||||
{
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.From<Transaction>().Update(transaction);
|
||||
var result = await SupabaseService.Client.From<Transaction>().Update(transaction);
|
||||
if (result.Model is null) return;
|
||||
var item = Transactions.FirstOrDefault(x => x.Id == result.Model.Id);
|
||||
if (item is null) return;
|
||||
var index = Transactions.IndexOf(item);
|
||||
|
||||
if (index != -1) Transactions[index] = LinkTransactionCategories(result.Model);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -76,6 +111,9 @@ public class GeneralDataRepo
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.From<Transaction>().Where(x => x.Id == id).Delete();
|
||||
var item = Transactions.FirstOrDefault(x => x.Id == id);
|
||||
if (item is null) return;
|
||||
Transactions.Remove(item);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -84,60 +122,62 @@ public class GeneralDataRepo
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Category>> FetchCategories()
|
||||
public async Task<List<Category>> FetchCategories(bool forceRefresh = false)
|
||||
{
|
||||
if (Categories is not null) return Categories;
|
||||
if (Categories.Count != 0 && !forceRefresh) return Categories.ToList();
|
||||
var categories = await SupabaseService.Client.From<Category>().Get();
|
||||
Categories = categories.Models;
|
||||
Categories = new ObservableCollection<Category>(categories.Models);
|
||||
return categories.Models;
|
||||
}
|
||||
|
||||
public async Task<List<Account>> FetchAccounts()
|
||||
public async Task<List<Account>> FetchAccounts(bool forceRefresh = false)
|
||||
{
|
||||
if (Accounts is not null) return Accounts;
|
||||
if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
|
||||
var accounts = await SupabaseService.Client.From<Account>().Get();
|
||||
Accounts = accounts.Models;
|
||||
Accounts = new ObservableCollection<Account>(accounts.Models);
|
||||
return accounts.Models;
|
||||
}
|
||||
|
||||
public async Task<List<Budget>> FetchBudgets()
|
||||
public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
|
||||
{
|
||||
if (Budgets is not null) return Budgets;
|
||||
if (Budgets.Count != 0 && !forceRefresh) return Budgets.ToList();
|
||||
var budgets = await SupabaseService.Client.From<Budget>().Get();
|
||||
Budgets = budgets.Models;
|
||||
Budgets = new ObservableCollection<Budget>(budgets.Models);
|
||||
return budgets.Models;
|
||||
}
|
||||
|
||||
public async Task<List<Budget>> FetchProcessedBudgets(DateTime CurrentPeriod)
|
||||
{
|
||||
var categories = await FetchCategories();
|
||||
var transactions = await FetchTransactions();
|
||||
var budgets = await FetchBudgets();
|
||||
var budgets = Budgets;
|
||||
var outputList = new List<Budget>();
|
||||
foreach (var budget in budgets)
|
||||
{
|
||||
budget.Category = categories.FirstOrDefault(x => x.Id == budget.CategoryId);
|
||||
budget.Category = Categories.FirstOrDefault(x => x.Id == budget.CategoryId);
|
||||
|
||||
switch (budget.Period.ToLower())
|
||||
{
|
||||
case "monthly":
|
||||
var budgetTransactions = transactions.Where(x =>
|
||||
var budgetTransactions = Transactions.Where(x =>
|
||||
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
|
||||
budget.Spent = budgetTransactions.Sum(x => x.Amount);
|
||||
budget.TransactionsCount = budgetTransactions.Count;
|
||||
break;
|
||||
case "quarterly":
|
||||
var quarterTransactions = transactions.Where(x =>
|
||||
var quarterTransactions = Transactions.Where(x =>
|
||||
x.Date.Month >= CurrentPeriod.Month - 3 && x.Date.Month <= CurrentPeriod.Month && x.CategoryId == budget.CategoryId).ToList();
|
||||
budget.Spent = quarterTransactions.Sum(x => x.Amount);
|
||||
budget.TransactionsCount = quarterTransactions.Count;
|
||||
break;
|
||||
case "yearly":
|
||||
var yearTransactions = transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
|
||||
var yearTransactions = Transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
|
||||
budget.Spent = yearTransactions.Sum(x => x.Amount);
|
||||
budget.TransactionsCount = yearTransactions.Count;
|
||||
break;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(budget.IsOnTrack));
|
||||
OnPropertyChanged(nameof(budget.IsWarning));
|
||||
OnPropertyChanged(nameof(budget.IsOverBudget));
|
||||
}
|
||||
|
||||
|
||||
@@ -173,4 +213,239 @@ public class GeneralDataRepo
|
||||
|
||||
return outputList;
|
||||
}
|
||||
|
||||
public async Task<Account?> InsertAccount(Account account)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Account>()
|
||||
.Insert(account, new QueryOptions() { Returning = QueryOptions.ReturnType.Representation });
|
||||
if (result.Model is null) return null;
|
||||
Accounts.Add(result.Model);
|
||||
return result.Model;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAccount(Account account)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Account>().Update(account);
|
||||
if (result.Model is null) return;
|
||||
var item = Accounts.FirstOrDefault(x => x.Id == result.Model.Id);
|
||||
if (item is null) return;
|
||||
var index = Accounts.IndexOf(item);
|
||||
if (index != -1) Accounts[index] = result.Model;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task MigrateTransactions(Guid accountId, Guid targetAccountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var update = await SupabaseService.Client
|
||||
.From<Transaction>()
|
||||
.Where(x => x.AccountId == accountId)
|
||||
.Set(x => x.AccountId, targetAccountId)
|
||||
.Update();
|
||||
foreach (var updateModel in update.Models)
|
||||
{
|
||||
var item = Transactions.SingleOrDefault(x => x.Id == updateModel.Id);
|
||||
if (item is null) return;
|
||||
var index = Transactions.IndexOf(item);
|
||||
if (index != -1) Transactions[index] = updateModel;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RecalculateAccountBalance(Guid targetAccountId)
|
||||
{
|
||||
var accountResult = Accounts
|
||||
.SingleOrDefault(a => a.Id == targetAccountId);
|
||||
|
||||
if (accountResult is null) return;
|
||||
|
||||
var transactionsResult = Transactions
|
||||
.Where(t => t.AccountId == targetAccountId);
|
||||
|
||||
var balance = accountResult.OpeningBalance +
|
||||
transactionsResult.Sum(t =>
|
||||
t.Type == "income" ? t.Amount : -t.Amount);
|
||||
|
||||
accountResult.CurrentBalance = balance;
|
||||
await SupabaseService.Client
|
||||
.From<Account>()
|
||||
.Update(accountResult);
|
||||
var index = Accounts.IndexOf(accountResult);
|
||||
if (index != -1) Accounts[index] = accountResult;
|
||||
}
|
||||
|
||||
public async Task DeleteAccount(Guid accountId)
|
||||
{
|
||||
await SupabaseService.Client
|
||||
.From<Account>()
|
||||
.Where(a => a.Id == accountId)
|
||||
.Delete();
|
||||
|
||||
var item = Accounts.FirstOrDefault(x => x.Id == accountId);
|
||||
if (item is null) return;
|
||||
Accounts.Remove(item);
|
||||
}
|
||||
|
||||
public async Task InsertBudget(Budget budget)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Budget>().Insert(budget);
|
||||
if (result.Models.Count >= 1)
|
||||
{
|
||||
Budgets.Add(result.Models[0]);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateBudget(Budget budget)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Budget>().Update(budget);
|
||||
if (result.Model is null) return;
|
||||
var item = Budgets.FirstOrDefault(x => x.Id == result.Model.Id);
|
||||
if (item is null) return;
|
||||
var index = Budgets.IndexOf(item);
|
||||
if (index != -1) Budgets[index] = result.Model;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteBudget(Guid BudgetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.From<Budget>().Where(x => x.Id == BudgetId).Delete();
|
||||
var item = Budgets.FirstOrDefault(x => x.Id == BudgetId);
|
||||
if (item is null) return;
|
||||
Budgets.Remove(item);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void LinkTransactionCategories()
|
||||
{
|
||||
foreach (var transaction in Transactions)
|
||||
{
|
||||
transaction.Category = Categories.FirstOrDefault(x => x.Id == transaction.CategoryId);
|
||||
}
|
||||
}
|
||||
|
||||
public Transaction LinkTransactionCategories(Transaction transaction)
|
||||
{
|
||||
transaction.Category = Categories.FirstOrDefault(x => x.Id == transaction.CategoryId);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public async Task UpdateProfile(Profile profile)
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Profile>().Update(profile);
|
||||
if (result.Models.Count > 0) Profile = result.Models[0];
|
||||
}
|
||||
|
||||
public async Task UpdateProfileAvatar(string? avatarUrl)
|
||||
{
|
||||
var profile = Profile;
|
||||
profile.AvatarUrl = avatarUrl;
|
||||
|
||||
var result = await SupabaseService.Client
|
||||
.From<Profile>()
|
||||
.Update(profile);
|
||||
Profile = result.Models[0];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Upload a local file as the current user's avatar. Returns the public URL.</summary>
|
||||
public async Task<string> UploadAvatarAsync(string localFilePath)
|
||||
{
|
||||
var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
|
||||
var ext = Path.GetExtension(localFilePath).ToLowerInvariant();
|
||||
var storagePath = $"{userId}/avatar{ext}";
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(localFilePath);
|
||||
var mimeType = ext switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".webp" => "image/webp",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
|
||||
var bucket = SupabaseService.Client.Storage.From(Bucket);
|
||||
|
||||
// Upsert: upload if not exists, replace if it does
|
||||
await bucket.Upload(bytes, storagePath, new FileOptions
|
||||
{
|
||||
ContentType = mimeType,
|
||||
Upsert = true
|
||||
});
|
||||
|
||||
var stream = new MemoryStream(bytes);
|
||||
// Append cache-buster so Avalonia Image re-fetches the new file
|
||||
return $"{PublicBaseUrl}/{storagePath}?t={DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
||||
}
|
||||
|
||||
/// <summary>Delete the current user's avatar from storage.</summary>
|
||||
public async Task DeleteAvatarAsync()
|
||||
{
|
||||
var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
|
||||
|
||||
// Try both extensions since we don't track which was uploaded
|
||||
var bucket = SupabaseService.Client.Storage.From(Bucket);
|
||||
foreach (var ext in new[] { "jpg", "jpeg", "png", "webp" })
|
||||
{
|
||||
try
|
||||
{
|
||||
await bucket.Remove([$"{userId}/avatar.{ext}"]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* file with that ext may not exist, ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Build the public URL for a given avatar_url stored in the profile.</summary>
|
||||
public string? BuildPublicUrl(string? avatarUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl)) return null;
|
||||
// If already a full URL (from storage or external), return as-is
|
||||
if (avatarUrl.StartsWith("http")) return avatarUrl;
|
||||
return $"{PublicBaseUrl}/{avatarUrl}";
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:views="clr-namespace:Clario.Views"
|
||||
xmlns:model="clr-namespace:Clario.Models"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.BudgetViewMobile"
|
||||
@@ -255,12 +254,6 @@
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="BottomEdgeAlignedRight"
|
||||
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
|
||||
<views:BudgetCardMenuView />
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
<Svg Path="../Assets/Icons/ellipsis.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
@@ -434,7 +427,7 @@
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold"
|
||||
<TextBlock Grid.Column="1" Text="{Binding AppData.Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
@@ -447,7 +440,7 @@
|
||||
<ProgressBar Classes="yellow"
|
||||
Value="{Binding TotalLeft}"
|
||||
Minimum="0"
|
||||
Maximum="{Binding Profile.SavingsGoal}"
|
||||
Maximum="{Binding AppData.Profile.SavingsGoal}"
|
||||
Height="6" />
|
||||
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
|
||||
@@ -30,7 +30,7 @@ public class Account : BaseModel
|
||||
|
||||
[Column("is_archived")] public bool IsArchived { get; set; }
|
||||
|
||||
[Column("opened_at")] public DateOnly? OpenedAt { get; set; }
|
||||
[Column("opened_at")] public DateTime? OpenedAt { get; set; }
|
||||
|
||||
[Column("created_at")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
@@ -45,5 +45,7 @@ public class Account : BaseModel
|
||||
[JsonIgnore] public decimal TotalExpenseThisMonth { get; set; }
|
||||
[JsonIgnore] public decimal MonthlyIncrease { get; set; }
|
||||
[JsonIgnore] public List<Transaction>? RecentTransactions { get; set; }
|
||||
[JsonIgnore] public bool isCredit => Type == "Credit";
|
||||
[JsonIgnore] public decimal CreditUtilizationPerc => (CurrentBalance < 0 ? CurrentBalance * -1 : 0) / (CreditLimit == 0 ? 1 : CreditLimit) ?? 1;
|
||||
[JsonIgnore] public bool GroupHeader { get; set; } = false;
|
||||
}
|
||||
@@ -29,13 +29,13 @@ public class Budget : BaseModel
|
||||
|
||||
[JsonIgnore] public Category? Category { get; set; }
|
||||
[JsonIgnore] public int TransactionsCount { get; set; }
|
||||
[JsonIgnore] public decimal Spent { get; set; } // populated after joining with transactions
|
||||
[JsonIgnore] public decimal Spent { get; set; }
|
||||
|
||||
[JsonIgnore] public decimal Remaining => LimitAmount - Spent;
|
||||
[JsonIgnore] public double PercentageUsed => LimitAmount > 0 ? Math.Round((double)(Spent / LimitAmount), 2) : 0;
|
||||
[JsonIgnore] public bool IsOverBudget => Spent > LimitAmount;
|
||||
[JsonIgnore] public bool IsWarning => !IsOverBudget && PercentageUsed * 100 >= AlertThreshold;
|
||||
[JsonIgnore] public bool IsOnTrack => !IsOverBudget && PercentageUsed * 100 < AlertThreshold;
|
||||
[JsonIgnore] public bool IsOnTrack => PercentageUsed * 100 < AlertThreshold;
|
||||
|
||||
[JsonIgnore] public string SpentFormatted => $"${Spent:N0}";
|
||||
[JsonIgnore] public string AmountFormatted => $"of ${LimitAmount:N0}";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Newtonsoft.Json;
|
||||
using Supabase.Postgrest.Attributes;
|
||||
using Supabase.Postgrest.Models;
|
||||
|
||||
@@ -10,6 +12,8 @@ public class Profile : BaseModel
|
||||
[PrimaryKey("id", false)] public Guid Id { get; set; }
|
||||
[Column("display_name")] public string DisplayName { get; set; }
|
||||
[Column("avatar_url")] public string? AvatarUrl { get; set; }
|
||||
[JsonIgnore] public Bitmap? Avatar { get; set; }
|
||||
[JsonIgnore] public bool HasAvatar => !string.IsNullOrWhiteSpace(AvatarUrl);
|
||||
[Column("currency")] public string Currency { get; set; }
|
||||
[Column("theme")] public string Theme { get; set; }
|
||||
[Column("language")] public string Language { get; set; }
|
||||
|
||||
@@ -16,19 +16,7 @@ public class Transaction : BaseModel
|
||||
|
||||
[Column("account_id")] public Guid AccountId { get; set; }
|
||||
|
||||
private Guid? _categoryId;
|
||||
|
||||
[Column("category_id")]
|
||||
public Guid? CategoryId
|
||||
{
|
||||
get => _categoryId;
|
||||
set
|
||||
{
|
||||
_categoryId = value;
|
||||
|
||||
Category = DataRepo.General.FetchCategories().Result.FirstOrDefault(x => x.Id == value);
|
||||
}
|
||||
}
|
||||
[Column("category_id")] public Guid? CategoryId { get; set; }
|
||||
|
||||
[JsonIgnore] public Category? Category { get; set; }
|
||||
|
||||
|
||||
78
Clario/Services/AvatarService.cs
Normal file
78
Clario/Services/AvatarService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
// using System;
|
||||
// using System.IO;
|
||||
// using System.Threading.Tasks;
|
||||
// using Avalonia.Media.Imaging;
|
||||
// using Clario.Data;
|
||||
// using Supabase.Storage;
|
||||
// using FileOptions = Supabase.Storage.FileOptions;
|
||||
//
|
||||
// namespace Clario.Services;
|
||||
//
|
||||
// public class AvatarService
|
||||
// {
|
||||
// public static AvatarService Instance = new();
|
||||
//
|
||||
// private const string Bucket = "avatars";
|
||||
// private const string ProjectRef = "xzxstbllaivumhtpctmo";
|
||||
// private const string PublicBaseUrl = $"https://{ProjectRef}.supabase.co/storage/v1/object/public/{Bucket}";
|
||||
//
|
||||
// /// <summary>Upload a local file as the current user's avatar. Returns the public URL.</summary>
|
||||
// public async Task<string> UploadAvatarAsync(string localFilePath)
|
||||
// {
|
||||
// var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
|
||||
// var ext = Path.GetExtension(localFilePath).ToLowerInvariant();
|
||||
// var storagePath = $"{userId}/avatar{ext}";
|
||||
//
|
||||
// var bytes = await File.ReadAllBytesAsync(localFilePath);
|
||||
// var mimeType = ext switch
|
||||
// {
|
||||
// ".jpg" or ".jpeg" => "image/jpeg",
|
||||
// ".png" => "image/png",
|
||||
// ".webp" => "image/webp",
|
||||
// _ => "application/octet-stream"
|
||||
// };
|
||||
//
|
||||
// var bucket = SupabaseService.Client.Storage.From(Bucket);
|
||||
//
|
||||
// // Upsert: upload if not exists, replace if it does
|
||||
// await bucket.Upload(bytes, storagePath, new FileOptions
|
||||
// {
|
||||
// ContentType = mimeType,
|
||||
// Upsert = true
|
||||
// });
|
||||
//
|
||||
// var stream = new MemoryStream(bytes);
|
||||
// DataRepo.General.Profile!.Avatar = new Bitmap(stream);
|
||||
// // Append cache-buster so Avalonia Image re-fetches the new file
|
||||
// return $"{PublicBaseUrl}/{storagePath}?t={DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
||||
// }
|
||||
//
|
||||
// /// <summary>Delete the current user's avatar from storage.</summary>
|
||||
// public async Task DeleteAvatarAsync()
|
||||
// {
|
||||
// var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
|
||||
//
|
||||
// // Try both extensions since we don't track which was uploaded
|
||||
// var bucket = SupabaseService.Client.Storage.From(Bucket);
|
||||
// foreach (var ext in new[] { "jpg", "jpeg", "png", "webp" })
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// await bucket.Remove([$"{userId}/avatar.{ext}"]);
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// /* file with that ext may not exist, ignore */
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>Build the public URL for a given avatar_url stored in the profile.</summary>
|
||||
// public static string? BuildPublicUrl(string? avatarUrl)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(avatarUrl)) return null;
|
||||
// // If already a full URL (from storage or external), return as-is
|
||||
// if (avatarUrl.StartsWith("http")) return avatarUrl;
|
||||
// return $"{PublicBaseUrl}/{avatarUrl}";
|
||||
// }
|
||||
// }
|
||||
39
Clario/Services/FilePickerService.cs
Normal file
39
Clario/Services/FilePickerService.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform.Storage;
|
||||
|
||||
namespace Clario.Services;
|
||||
|
||||
public class FilePickerService
|
||||
{
|
||||
public static FilePickerService Instance { get; } = new();
|
||||
private static TopLevel? GetTopLevel()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return TopLevel.GetTopLevel(desktop.MainWindow);
|
||||
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime single)
|
||||
return TopLevel.GetTopLevel(single.MainView as Visual);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IStorageFile?> PickImageAsync()
|
||||
{
|
||||
var topLevel = GetTopLevel();
|
||||
if (topLevel is null) return null;
|
||||
|
||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Select Avatar Image",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new("Images") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.webp"] }
|
||||
}
|
||||
});
|
||||
|
||||
return files.Count > 0 ? files[0] : null;
|
||||
}
|
||||
}
|
||||
@@ -9,22 +9,18 @@ public class FileSessionStorage : ISessionStorage
|
||||
|
||||
public void Save(string json)
|
||||
{
|
||||
// Console.WriteLine($"Saving session to {_path}");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
File.WriteAllText(_path, json);
|
||||
}
|
||||
|
||||
public string? Load()
|
||||
{
|
||||
if (!File.Exists(_path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (!File.Exists(_path)) return null;
|
||||
|
||||
var json = File.ReadAllText(_path);
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
<StyleInclude Source="Styles/ToggleSwitchStyles.axaml" />
|
||||
<!-- <StyleInclude Source="Styles/CalenderItemStyles.axaml" /> -->
|
||||
<StyleInclude Source="Styles/ColorPickerStyles.axaml" />
|
||||
<StyleInclude Source="Styles/CalendarStyles.axaml" />
|
||||
<StyleInclude Source="Styles/SliderStyles.axaml" />
|
||||
<StyleInclude Source="../CustomControls/DateRangePicker.axaml" />
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
@@ -1045,5 +1046,56 @@
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Budget Card — On Track ─────────────────────── -->
|
||||
<Style Selector="Border.budget-card">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Budget Card — Warning ──────────────────────── -->
|
||||
<Style Selector="Border.budget-card-warning">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentYellow}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Budget Card — Over Budget ─────────────────── -->
|
||||
<Style Selector="Border.budget-card-over">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentRed}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Progress Bar — Yellow ─────────────────────── -->
|
||||
<Style Selector="ProgressBar.yellow /template/ Border#PART_Indicator">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentYellow}" />
|
||||
<Setter Property="CornerRadius" Value="3" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Badge — Warning ───────────────────────────── -->
|
||||
<Style Selector="Border.badge-warning">
|
||||
<Setter Property="Background" Value="{DynamicResource BadgeBgYellow}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentYellow}" />
|
||||
<Setter Property="CornerRadius" Value="20" />
|
||||
<Setter Property="Padding" Value="6,2" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Badge — Over ──────────────────────────────── -->
|
||||
<Style Selector="Border.badge-over">
|
||||
<Setter Property="Background" Value="{DynamicResource BadgeBgRed}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentRed}" />
|
||||
<Setter Property="CornerRadius" Value="20" />
|
||||
<Setter Property="Padding" Value="6,2" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
210
Clario/Theme/Styles/ColorPickerStyles.axaml
Normal file
210
Clario/Theme/Styles/ColorPickerStyles.axaml
Normal file
@@ -0,0 +1,210 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:primitives="using:Avalonia.Controls.Primitives">
|
||||
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="20" Background="#0D0F14">
|
||||
<ColorPicker Width="200" Height="36"/>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
RESOURCE OVERRIDES
|
||||
These override the Fluent resource keys used internally
|
||||
by the ColorPicker flyout template.
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Styles.Resources>
|
||||
<!-- Tab strip background (top 48px bar) -->
|
||||
<SolidColorBrush x:Key="SystemControlBackgroundBaseLowBrush" Color="#13161E"/>
|
||||
<!-- Tab strip border -->
|
||||
<SolidColorBrush x:Key="ColorViewTabBorderBrush" Color="#1E2330"/>
|
||||
<!-- Content area background (below tab strip) -->
|
||||
<SolidColorBrush x:Key="ColorViewContentBackgroundBrush" Color="#13161E"/>
|
||||
<!-- Content area border (the top-border line between tabs and content) -->
|
||||
<SolidColorBrush x:Key="ColorViewContentBorderBrush" Color="#1E2330"/>
|
||||
|
||||
<!-- Fluent text control resources used by component label borders + hex # border -->
|
||||
<SolidColorBrush x:Key="TextControlBackgroundDisabled" Color="#1A1E2A"/>
|
||||
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#1E2330"/>
|
||||
<SolidColorBrush x:Key="TextControlForegroundDisabled" Color="#7A8090"/>
|
||||
|
||||
<!-- TextBox (hex input + component spinners) -->
|
||||
<SolidColorBrush x:Key="TextControlBackground" Color="#13161E"/>
|
||||
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#1A1E2A"/>
|
||||
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#13161E"/>
|
||||
<SolidColorBrush x:Key="TextControlForeground" Color="#F0F2F8"/>
|
||||
<SolidColorBrush x:Key="TextControlForegroundPointerOver" Color="#F0F2F8"/>
|
||||
<SolidColorBrush x:Key="TextControlForegroundFocused" Color="#F0F2F8"/>
|
||||
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#2A3050"/>
|
||||
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#7B9CFF"/>
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#5A6070"/>
|
||||
|
||||
<!-- RadioButton (RGB/HSV toggle) -->
|
||||
<SolidColorBrush x:Key="RadioButtonBackground" Color="#13161E"/>
|
||||
<SolidColorBrush x:Key="RadioButtonBackgroundPointerOver" Color="#1A1E2A"/>
|
||||
<SolidColorBrush x:Key="RadioButtonBackgroundPressed" Color="#1E2330"/>
|
||||
<SolidColorBrush x:Key="RadioButtonBorderBrush" Color="#1E2330"/>
|
||||
<SolidColorBrush x:Key="RadioButtonBorderBrushPointerOver" Color="#2A3050"/>
|
||||
<SolidColorBrush x:Key="RadioButtonForeground" Color="#C8D0E8"/>
|
||||
<SolidColorBrush x:Key="RadioButtonForegroundPointerOver" Color="#F0F2F8"/>
|
||||
<SolidColorBrush x:Key="RadioButtonOuterEllipseStroke" Color="#7B9CFF"/>
|
||||
<SolidColorBrush x:Key="RadioButtonOuterEllipseCheckedStroke" Color="#7B9CFF"/>
|
||||
<SolidColorBrush x:Key="RadioButtonOuterEllipseCheckedFill" Color="#7B9CFF"/>
|
||||
<SolidColorBrush x:Key="RadioButtonCheckGlyphFill" Color="#0D0F14"/>
|
||||
|
||||
<!-- ColorPreviewer -->
|
||||
<SolidColorBrush x:Key="ColorViewPreviewBorderBrush" Color="#1E2330"/>
|
||||
</Styles.Resources>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
ColorPicker — the drop-down button itself
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="ColorPicker">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
</Style>
|
||||
|
||||
<!-- The DropDownButton inside ColorPicker -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton">
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton:pointerover /template/ Border#Background">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton:pressed /template/ Border#Background">
|
||||
<Setter Property="Background" Value="{DynamicResource BorderSubtle}"/>
|
||||
</Style>
|
||||
|
||||
<!-- The chevron arrow inside DropDownButton -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ PathIcon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
Flyout popup wrapper
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="FlyoutPresenter.nopadding">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
|
||||
<!-- <Setter Property="BoxShadow" Value="0 8 32 0 #3C000000"/> -->
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
Tab strip inside the flyout
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabControl">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
</Style>
|
||||
|
||||
<!-- TabItem (spectrum / palette / components icons) -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="MinHeight" Value="48"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem:selected">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem:pointerover">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
</Style>
|
||||
|
||||
<!-- PathIcon inside tab headers -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem PathIcon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem:selected PathIcon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
Hex input TextBox
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="0,4,4,0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox:focus /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentBlue}"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox:pointerover /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
NumericUpDown (RGB/HSV component value inputs)
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup NumericUpDown">
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup NumericUpDown /template/ Border">
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
ColorSlider (hue, saturation, value sliders)
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="primitives|ColorSlider">
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Height" Value="16"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Slider thumb -->
|
||||
<Style Selector="primitives|ColorSlider /template/ Thumb">
|
||||
<Setter Property="Width" Value="16"/>
|
||||
<Setter Property="Height" Value="16"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TextPrimary}"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
</Style>
|
||||
<Style Selector="primitives|ColorSlider /template/ Thumb:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBlue}"/>
|
||||
</Style>
|
||||
<Style Selector="primitives|ColorSlider /template/ Thumb:pressed">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBlue}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Vertical slider (the third component slider beside the spectrum) -->
|
||||
<Style Selector="primitives|ColorSlider[Orientation=Vertical]">
|
||||
<Setter Property="Width" Value="16"/>
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
ColorPreviewer (accent color swatches at the bottom)
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<Style Selector="primitives|ColorPreviewer">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Individual preview color swatch borders -->
|
||||
<Style Selector="primitives|ColorPreviewer Border">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
116
Clario/Theme/Styles/SliderStyles.axaml
Normal file
116
Clario/Theme/Styles/SliderStyles.axaml
Normal file
@@ -0,0 +1,116 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="24" Background="#0D0F14" Width="320">
|
||||
<StackPanel Spacing="24">
|
||||
<Slider Minimum="0" Maximum="100" Value="30" />
|
||||
<Slider Minimum="0" Maximum="100" Value="65" />
|
||||
<Slider Minimum="0" Maximum="100" Value="80" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<!-- Add to SliderStyles.axaml inside <Styles.Resources> -->
|
||||
<Styles.Resources>
|
||||
<ControlTheme x:Key="SliderRepeatButton" TargetType="RepeatButton">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="2"
|
||||
Height="4"
|
||||
VerticalAlignment="Center" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type Slider}" TargetType="Slider">
|
||||
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}" />
|
||||
|
||||
<Style Selector="^:horizontal">
|
||||
<Setter Property="MinHeight" Value="20" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="grid">
|
||||
<Border x:Name="TrackBackground"
|
||||
Height="4"
|
||||
Margin="8,0"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
CornerRadius="2"
|
||||
VerticalAlignment="Center" />
|
||||
<Track x:Name="PART_Track"
|
||||
Minimum="{TemplateBinding Minimum}"
|
||||
Maximum="{TemplateBinding Maximum}"
|
||||
Value="{TemplateBinding Value, Mode=TwoWay}"
|
||||
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
|
||||
Orientation="Horizontal">
|
||||
<Track.DecreaseButton>
|
||||
<RepeatButton x:Name="PART_DecreaseButton"
|
||||
Background="{DynamicResource AccentBlue}"
|
||||
Theme="{StaticResource SliderRepeatButton}" />
|
||||
</Track.DecreaseButton>
|
||||
<Track.IncreaseButton>
|
||||
<RepeatButton x:Name="PART_IncreaseButton"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
Theme="{StaticResource SliderRepeatButton}" />
|
||||
</Track.IncreaseButton>
|
||||
<Thumb x:Name="thumb" Width="16" Height="16">
|
||||
<Thumb.Template>
|
||||
<ControlTemplate>
|
||||
<Border Width="16" Height="16"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource AccentBlue}"
|
||||
BorderBrush="{DynamicResource BgSurface}"
|
||||
BorderThickness="2"
|
||||
BoxShadow="0 2 8 0 #40000000" />
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Pointer over -->
|
||||
<Style Selector="^:pointerover /template/ RepeatButton#PART_DecreaseButton">
|
||||
<Setter Property="Background" Value="#8FAEFF" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover /template/ Thumb#thumb">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Width="16" Height="16" CornerRadius="8"
|
||||
Background="#8FAEFF"
|
||||
BorderBrush="{DynamicResource BgSurface}"
|
||||
BorderThickness="2"
|
||||
BoxShadow="0 2 12 0 #507B9CFF" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Pressed -->
|
||||
<Style Selector="^:pressed /template/ RepeatButton#PART_DecreaseButton">
|
||||
<Setter Property="Background" Value="#6B8AEF" />
|
||||
</Style>
|
||||
<Style Selector="^:pressed /template/ Thumb#thumb">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Width="14" Height="14" CornerRadius="7"
|
||||
Background="#6B8AEF"
|
||||
BorderBrush="{DynamicResource BgSurface}"
|
||||
BorderThickness="2" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Disabled -->
|
||||
<Style Selector="^:disabled /template/ Grid#grid">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
</ControlTheme>
|
||||
</Styles.Resources>
|
||||
|
||||
</Styles>
|
||||
210
Clario/ViewModels/AccountFormViewModel.cs
Normal file
210
Clario/ViewModels/AccountFormViewModel.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class AccountFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
|
||||
private bool _isEditMode = false;
|
||||
|
||||
public string FormTitle => IsEditMode ? "Edit Account" : "New Account";
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Account";
|
||||
|
||||
// ── Fields ──────────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private string _name = "";
|
||||
|
||||
[ObservableProperty] private string _selectedType = "Checking";
|
||||
|
||||
[ObservableProperty] private string? _institution;
|
||||
|
||||
[ObservableProperty] private string? _mask;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private string _openingBalance = "0.00";
|
||||
|
||||
[ObservableProperty] private string _currency = "USD";
|
||||
|
||||
[ObservableProperty] private string? _creditLimit;
|
||||
|
||||
[ObservableProperty] private List<DateTime>? _openedAtDates;
|
||||
|
||||
[ObservableProperty] private string _selectedIcon = "wallet";
|
||||
|
||||
[ObservableProperty] private string _selectedColor = "#3B82F6";
|
||||
|
||||
// ── Options ─────────────────────────────────────────────
|
||||
[ObservableProperty] private List<string> _accountTypes = new() { "Cash", "Checking", "Savings", "Credit", "Investment", "Other" };
|
||||
|
||||
[ObservableProperty] private List<string> _currencies = new() { "USD", "EUR", "GBP", "CAD", "AUD" };
|
||||
|
||||
[ObservableProperty] private List<string> _icons = new() { "wallet", "credit-card", "banknote", "landmark", "piggy-bank", "dollar-sign" };
|
||||
|
||||
// ── Validation ──────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
|
||||
public bool IsValid =>
|
||||
!string.IsNullOrWhiteSpace(Name) &&
|
||||
decimal.TryParse(OpeningBalance, out _);
|
||||
|
||||
public bool IsCredit => SelectedType == "Credit";
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
|
||||
// ── Edit mode: original account ─────────────────────────
|
||||
private Guid? _editingId;
|
||||
|
||||
// ── Result account ──────────────────────────────────────
|
||||
public Account? ResultAccount { get; set; }
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
|
||||
partial void OnSelectedTypeChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsCredit));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Save()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
ErrorMessage = "Name is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(OpeningBalance, out var balance))
|
||||
{
|
||||
ErrorMessage = "Please enter a valid opening balance.";
|
||||
return;
|
||||
}
|
||||
|
||||
decimal? creditLimitValue = null;
|
||||
if (IsCredit && !string.IsNullOrWhiteSpace(CreditLimit))
|
||||
{
|
||||
if (!decimal.TryParse(CreditLimit, out var limit))
|
||||
{
|
||||
ErrorMessage = "Please enter a valid credit limit.";
|
||||
return;
|
||||
}
|
||||
|
||||
creditLimitValue = limit;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (IsEditMode && _editingId.HasValue)
|
||||
{
|
||||
var updated = new Account
|
||||
{
|
||||
Id = _editingId.Value,
|
||||
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id),
|
||||
Name = Name.Trim(),
|
||||
Type = SelectedType,
|
||||
Institution = Institution?.Trim(),
|
||||
Mask = Mask?.Trim(),
|
||||
Currency = Currency,
|
||||
OpeningBalance = balance,
|
||||
CreditLimit = creditLimitValue,
|
||||
OpenedAt = OpenedAtDates?[0],
|
||||
Icon = SelectedIcon,
|
||||
Color = SelectedColor,
|
||||
};
|
||||
await DataRepo.General.UpdateAccount(updated);
|
||||
ResultAccount = updated;
|
||||
}
|
||||
else
|
||||
{
|
||||
var account = new Account
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id!),
|
||||
Name = Name.Trim(),
|
||||
Type = SelectedType,
|
||||
Institution = Institution?.Trim(),
|
||||
Mask = Mask?.Trim(),
|
||||
Currency = Currency,
|
||||
OpeningBalance = balance,
|
||||
CreditLimit = creditLimitValue,
|
||||
OpenedAt = OpenedAtDates?[0],
|
||||
Icon = SelectedIcon,
|
||||
Color = SelectedColor,
|
||||
};
|
||||
var result = await DataRepo.General.InsertAccount(account);
|
||||
ResultAccount = result;
|
||||
}
|
||||
|
||||
OnSaved?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Something went wrong. Please try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
OnCancelled?.Invoke();
|
||||
}
|
||||
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
|
||||
/// <summary>Call this to open the form for adding a new account.</summary>
|
||||
public void SetupForAdd()
|
||||
{
|
||||
IsEditMode = false;
|
||||
_editingId = null;
|
||||
Name = "";
|
||||
SelectedType = "Checking";
|
||||
Institution = null;
|
||||
Mask = null;
|
||||
OpeningBalance = "0.00";
|
||||
Currency = DataRepo.General.Profile?.Currency ?? "USD";
|
||||
CreditLimit = null;
|
||||
OpenedAtDates = null;
|
||||
SelectedIcon = "wallet";
|
||||
SelectedColor = "#3B82F6";
|
||||
ErrorMessage = null;
|
||||
ResultAccount = null;
|
||||
}
|
||||
|
||||
/// <summary>Call this to open the form for editing an existing account.</summary>
|
||||
public void SetupForEdit(Account account)
|
||||
{
|
||||
IsEditMode = true;
|
||||
_editingId = account.Id;
|
||||
Name = account.Name;
|
||||
SelectedType = account.Type;
|
||||
Institution = account.Institution;
|
||||
Mask = account.Mask;
|
||||
OpeningBalance = account.OpeningBalance.ToString("0.00");
|
||||
Currency = account.Currency;
|
||||
CreditLimit = account.CreditLimit?.ToString("0.00");
|
||||
OpenedAtDates = account.OpenedAt.HasValue ? new List<DateTime> { account.OpenedAt.Value } : null;
|
||||
SelectedIcon = account.Icon;
|
||||
SelectedColor = account.Color;
|
||||
ErrorMessage = null;
|
||||
ResultAccount = account;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
@@ -13,18 +12,25 @@ namespace Clario.ViewModels;
|
||||
public partial class AccountsViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public required List<Account> Accounts = new();
|
||||
public required List<Transaction> Transactions = new();
|
||||
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Account> _visibleAccounts = new();
|
||||
[ObservableProperty] private decimal _totalBalance = 0;
|
||||
[ObservableProperty] private decimal _totalBalance;
|
||||
[ObservableProperty] private Account? _selectedAccount;
|
||||
[ObservableProperty] private bool _isAccountDeletionConfirmationVisible;
|
||||
public bool CanDeleteAccount => VisibleAccounts.Count > 1;
|
||||
|
||||
[ObservableProperty] private bool _isDeleteDialogVisible;
|
||||
[ObservableProperty] private DeleteAccountDialogViewModel _deleteDialog = new();
|
||||
|
||||
public AccountsViewModel()
|
||||
{
|
||||
|
||||
AppData.Accounts.CollectionChanged += (_, _) => { Initialize(); };
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
public void Initialize()
|
||||
{
|
||||
FetchAndProcessAccountInfo();
|
||||
GroupAccounts();
|
||||
@@ -33,9 +39,9 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
|
||||
private void FetchAndProcessAccountInfo()
|
||||
{
|
||||
foreach (var account in Accounts)
|
||||
foreach (var account in AppData.Accounts)
|
||||
{
|
||||
var accountTransactions = Transactions.Where(t => t.AccountId == account.Id).ToList();
|
||||
var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
|
||||
account.TransactionsCount = accountTransactions.Count;
|
||||
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
|
||||
account.TotalIncomeThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type == "income").Sum(t => t.Amount);
|
||||
@@ -50,22 +56,37 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateAccount()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddAccount();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditAccount(Account account)
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenEditAccount(account);
|
||||
}
|
||||
|
||||
|
||||
private void GroupAccounts()
|
||||
{
|
||||
var accountTypes = new Dictionary<string, string>()
|
||||
var accountTypes = new List<string>()
|
||||
{
|
||||
{ "checking", "Cash & Checking" },
|
||||
{ "savings", "Savings" },
|
||||
{ "credit", "Credit" },
|
||||
{ "investment", "Investments" }
|
||||
"Cash",
|
||||
"Checking",
|
||||
"Savings",
|
||||
"Credit",
|
||||
"Investment",
|
||||
"Other"
|
||||
};
|
||||
|
||||
VisibleAccounts.Clear();
|
||||
foreach (var type in accountTypes)
|
||||
{
|
||||
var accountsOfType = Accounts.Where(a => a.Type == type.Key).ToList();
|
||||
var accountsOfType = AppData.Accounts.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (accountsOfType.Any())
|
||||
{
|
||||
var header = new Account { Name = type.Value.ToUpper(), GroupHeader = true };
|
||||
var header = new Account { Name = type.ToUpper(), GroupHeader = true };
|
||||
VisibleAccounts.Add(header);
|
||||
foreach (var account in accountsOfType)
|
||||
{
|
||||
@@ -73,6 +94,21 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CanDeleteAccount));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RequestDeleteAccount(Account account)
|
||||
{
|
||||
DeleteDialog.Setup(account, new ObservableCollection<Account>(AppData.Accounts));
|
||||
DeleteDialog.OnDeleted = () =>
|
||||
{
|
||||
IsDeleteDialogVisible = false;
|
||||
Initialize();
|
||||
};
|
||||
DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false;
|
||||
IsDeleteDialogVisible = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -86,6 +122,7 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
{
|
||||
if (parentViewModel is MainViewModel mainViewModel)
|
||||
{
|
||||
if (SelectedAccount is null) return;
|
||||
var vm = mainViewModel._transactionsViewModel;
|
||||
vm.SelectedAccount = vm.Accounts.First(x => x.Id == SelectedAccount.Id);
|
||||
vm.LoadPageCommand.Execute(1);
|
||||
|
||||
@@ -40,11 +40,11 @@ public partial class AuthViewModel : ViewModelBase
|
||||
|
||||
private void setDefaults()
|
||||
{
|
||||
FirstName = "nouredeen";
|
||||
LastName = "ghazal";
|
||||
Email = "nouredeen.ghazal42@gmail.com";
|
||||
Password = "Nour1Clario";
|
||||
ConfirmPassword = "Nour1Clario";
|
||||
FirstName = "clario";
|
||||
LastName = "testing";
|
||||
Email = "Clario@testing.com";
|
||||
Password = "1234ABCD6767";
|
||||
ConfirmPassword = "1234ABCD6767";
|
||||
ThemeService.SwitchToTheme("system");
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public partial class AuthViewModel : ViewModelBase
|
||||
await SupabaseService.Client.Auth.SignIn(_email, _password);
|
||||
|
||||
var user = SupabaseService.Client.Auth.CurrentUser;
|
||||
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
@@ -99,7 +99,6 @@ public partial class AuthViewModel : ViewModelBase
|
||||
await SupabaseService.Client.Auth.SetSession(session.AccessToken, session.RefreshToken);
|
||||
var user = session.User;
|
||||
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class BudgetCardMenuViewModel : ViewModelBase
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,6 +1,210 @@
|
||||
namespace Clario.ViewModels;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class BudgetFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
|
||||
private bool _isEditMode = false;
|
||||
|
||||
public string FormTitle => IsEditMode ? "Edit Budget" : "New Budget";
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Budget";
|
||||
|
||||
// ── Fields ──────────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsMonthly), nameof(IsQuarterly), nameof(IsYearly), nameof(IsValid))]
|
||||
private string _period = "monthly";
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private string _limitAmount = "";
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private Category? _selectedCategory;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Category> _categories = new();
|
||||
|
||||
// AlertThreshold: 0–100 int, stored as double for Slider binding
|
||||
// Slider.Value is double; we round to int when saving
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(AlertThresholdLabel))]
|
||||
private double _alertThreshold = 80;
|
||||
|
||||
public string AlertThresholdLabel => $"{(int)AlertThreshold}%";
|
||||
|
||||
[ObservableProperty] private bool _rollover = false;
|
||||
|
||||
// ── Validation ──────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
|
||||
public bool IsMonthly => Period == "monthly";
|
||||
public bool IsQuarterly => Period == "quarterly";
|
||||
public bool IsYearly => Period == "yearly";
|
||||
|
||||
public bool IsValid =>
|
||||
decimal.TryParse(LimitAmount, out var amt) && amt > 0 &&
|
||||
SelectedCategory is not null;
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
public Action? OnDeleted;
|
||||
|
||||
[ObservableProperty] private bool _showDeleteConfirm = false;
|
||||
|
||||
// ── Edit mode: original budget ───────────────────────────
|
||||
private Guid? _editingId;
|
||||
|
||||
// ── Result ──────────────────────────────────────────────
|
||||
public Budget? ResultBudget { get; set; }
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void SetPeriod(string period)
|
||||
{
|
||||
Period = period;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Save()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
|
||||
if (!decimal.TryParse(LimitAmount, out var amt) || amt <= 0)
|
||||
{
|
||||
ErrorMessage = "Please enter a valid amount.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedCategory is null)
|
||||
{
|
||||
ErrorMessage = "Please select a category.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (IsEditMode && _editingId.HasValue)
|
||||
{
|
||||
var updated = new Budget
|
||||
{
|
||||
Id = _editingId.Value,
|
||||
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id),
|
||||
CategoryId = SelectedCategory.Id,
|
||||
LimitAmount = amt,
|
||||
Period = Period,
|
||||
AlertThreshold = (int)Math.Round(AlertThreshold),
|
||||
Rollover = Rollover,
|
||||
Category = SelectedCategory,
|
||||
};
|
||||
await DataRepo.General.UpdateBudget(updated);
|
||||
ResultBudget = updated;
|
||||
}
|
||||
else
|
||||
{
|
||||
var budget = new Budget
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id!),
|
||||
CategoryId = SelectedCategory.Id,
|
||||
LimitAmount = amt,
|
||||
Period = Period,
|
||||
AlertThreshold = (int)Math.Round(AlertThreshold),
|
||||
Rollover = Rollover,
|
||||
Category = SelectedCategory,
|
||||
};
|
||||
await DataRepo.General.InsertBudget(budget);
|
||||
ResultBudget = budget;
|
||||
}
|
||||
|
||||
OnSaved?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Something went wrong. Please try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmDelete()
|
||||
{
|
||||
if (!IsEditMode || !_editingId.HasValue) return;
|
||||
|
||||
try
|
||||
{
|
||||
await DataRepo.General.DeleteBudget(_editingId.Value);
|
||||
OnDeleted?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to delete budget.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RequestDelete()
|
||||
{
|
||||
ShowDeleteConfirm = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelDelete()
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
OnCancelled?.Invoke();
|
||||
}
|
||||
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
|
||||
/// <summary>Call this to open the form for adding a new budget.</summary>
|
||||
public void SetupForAdd(ObservableCollection<Category> categories)
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
IsEditMode = false;
|
||||
_editingId = null;
|
||||
Categories = categories;
|
||||
LimitAmount = "";
|
||||
Period = "monthly";
|
||||
AlertThreshold = 80;
|
||||
Rollover = false;
|
||||
ErrorMessage = null;
|
||||
SelectedCategory = categories.Count > 0 ? categories[0] : null;
|
||||
ResultBudget = null;
|
||||
}
|
||||
|
||||
/// <summary>Call this to open the form for editing an existing budget.</summary>
|
||||
public void SetupForEdit(Budget budget, ObservableCollection<Category> categories)
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
IsEditMode = true;
|
||||
_editingId = budget.Id;
|
||||
Categories = categories;
|
||||
LimitAmount = budget.LimitAmount.ToString("0.00");
|
||||
Period = budget.Period;
|
||||
AlertThreshold = budget.AlertThreshold;
|
||||
Rollover = budget.Rollover;
|
||||
ErrorMessage = null;
|
||||
SelectedCategory = categories.FirstOrDefault(c => c.Id == budget.CategoryId)
|
||||
?? (categories.Count > 0 ? categories[0] : null);
|
||||
ResultBudget = budget;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using Clario.Models.GeneralModels;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LiveChartsCore;
|
||||
@@ -20,17 +18,15 @@ namespace Clario.ViewModels;
|
||||
public partial class BudgetViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
[ObservableProperty] private Profile? _profile;
|
||||
public required List<Budget> Budgets = new();
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Budget> _visibleBudgets = new();
|
||||
public required List<Category> Categories = new();
|
||||
public required List<Transaction> Transactions = new();
|
||||
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NextPeriodCommand), nameof(PreviousPeriodCommand))]
|
||||
private DateTime _currentPeriod = DateTime.Now.Date;
|
||||
|
||||
public bool CanGoToNextPeriod => CurrentPeriod.Month < DateTime.Now.Month;
|
||||
public bool CanGoToPreviousPeriod => Transactions.Any() && CurrentPeriod.Month > Transactions.Min(x => x.Date.Month);
|
||||
public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && CurrentPeriod.Month > AppData.Transactions.Min(x => x.Date.Month);
|
||||
public string CurrentPeriodFormatted => CurrentPeriod.ToString("MMMM yyyy");
|
||||
|
||||
[ObservableProperty] private ISeries[] _spendingBreakdownChartSeries = [];
|
||||
@@ -43,9 +39,9 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue);
|
||||
public string TotalLeftFormatted => TotalLeft.ToString("C0") + " left";
|
||||
|
||||
public string SavingsHint => TotalLeft >= (Profile != null ? Profile.SavingsGoal : 0)
|
||||
public string SavingsHint => TotalLeft >= (AppData.Profile != null ? AppData.Profile.SavingsGoal : 0)
|
||||
? "You're on track!"
|
||||
: $"Reduce your spending by ${Math.Round((Profile != null ? Profile.SavingsGoal ?? 0 : 0) - TotalLeft)} to hit your goal.";
|
||||
: $"Reduce your spending by ${Math.Round((AppData.Profile != null ? AppData.Profile.SavingsGoal ?? 0 : 0) - TotalLeft)} to hit your goal.";
|
||||
|
||||
private int _onTrackCount;
|
||||
private int _approachingCount;
|
||||
@@ -60,13 +56,17 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
private int PeriodDaysLeft => PeriodLength - PeriodDaysPassed;
|
||||
public string PeriodDaysLeftFormatted => PeriodDaysLeft == 1 ? PeriodDaysLeft + " day left" : PeriodDaysLeft + " days left";
|
||||
|
||||
public string DailyBudgetLeftFormatted => ((TotalBudgeted - TotalSpent) / PeriodDaysLeft).ToString("C", new CultureInfo("en-US"));
|
||||
public string DailyBudgetLeftFormatted =>
|
||||
((TotalBudgeted - TotalSpent) / ((PeriodDaysLeft == 0) ? 1 : PeriodDaysLeft)).ToString("C", new CultureInfo("en-US"));
|
||||
|
||||
public BudgetViewModel()
|
||||
{
|
||||
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); };
|
||||
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); };
|
||||
_ = Initialize();
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
private async Task Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -80,15 +80,25 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateBudget()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddBudgetCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditBudget(Budget budget)
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget);
|
||||
}
|
||||
|
||||
private void ProcessChartData()
|
||||
{
|
||||
var categories = Categories;
|
||||
var transactions = Transactions;
|
||||
var tempCategorySpendingBreakdown = new List<(Category category, double[] spent)>();
|
||||
var tempSpendingBreakdownLegends = new List<Budget>();
|
||||
foreach (var category in categories)
|
||||
foreach (var category in AppData.Categories)
|
||||
{
|
||||
var spent = transactions
|
||||
var spent = AppData.Transactions
|
||||
.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase) &&
|
||||
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year)
|
||||
.Sum(x => x.Amount);
|
||||
@@ -115,11 +125,31 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
{
|
||||
VisibleBudgets.Clear();
|
||||
VisibleBudgets = new ObservableCollection<Budget>(await DataRepo.General.FetchProcessedBudgets(CurrentPeriod));
|
||||
_onTrackCount = VisibleBudgets.Count(x => x.IsOnTrack);
|
||||
_approachingCount = VisibleBudgets.Count(x => x.IsWarning);
|
||||
_overBudgetCount = VisibleBudgets.Count(x => x.IsOverBudget);
|
||||
_onTrackCount = VisibleBudgets.Count(x => x is { IsOnTrack: true, GroupHeader: false });
|
||||
_approachingCount = VisibleBudgets.Count(x => x is { IsWarning: true, GroupHeader: false });
|
||||
_overBudgetCount = VisibleBudgets.Count(x => x is { IsOverBudget: true, GroupHeader: false });
|
||||
TotalBudgeted = VisibleBudgets.Sum(x => x.LimitAmount);
|
||||
TotalSpent = VisibleBudgets.Sum(x => x.Spent);
|
||||
|
||||
NotifyComputedPropertiesOnChanged();
|
||||
}
|
||||
|
||||
private void NotifyComputedPropertiesOnChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(CanGoToNextPeriod));
|
||||
OnPropertyChanged(nameof(CanGoToPreviousPeriod));
|
||||
OnPropertyChanged(nameof(CurrentPeriodFormatted));
|
||||
OnPropertyChanged(nameof(SpentPercentageFormatted));
|
||||
OnPropertyChanged(nameof(TotalLeft));
|
||||
OnPropertyChanged(nameof(TotalLeftFormatted));
|
||||
OnPropertyChanged(nameof(SavingsHint));
|
||||
OnPropertyChanged(nameof(OnTrackCountFormatted));
|
||||
OnPropertyChanged(nameof(ApproachingCountFormatted));
|
||||
OnPropertyChanged(nameof(OverBudgetCountFormatted));
|
||||
OnPropertyChanged(nameof(PeriodLength));
|
||||
OnPropertyChanged(nameof(PeriodDaysPassed));
|
||||
OnPropertyChanged(nameof(PeriodDaysLeftFormatted));
|
||||
OnPropertyChanged(nameof(DailyBudgetLeftFormatted));
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanGoToNextPeriod))]
|
||||
|
||||
@@ -18,10 +18,8 @@ namespace Clario.ViewModels;
|
||||
public partial class DashboardViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public required List<Transaction> Transactions = new();
|
||||
public required List<Category> Categories = new();
|
||||
public required List<Budget> Budgets = new();
|
||||
public required List<Account> Accounts = new();
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
// public required List<Account> Accounts = new();
|
||||
|
||||
[ObservableProperty] private ObservableCollection<ColumnChartData> _spendingByCategoryChartData = new();
|
||||
[ObservableProperty] private ISeries[] _spendingByCategoryChartSeries = new ISeries[] { };
|
||||
@@ -31,19 +29,37 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
[ObservableProperty] private decimal _totalNetworth;
|
||||
[ObservableProperty] private decimal _monthlyIncome;
|
||||
private decimal _monthlyIncomeChange;
|
||||
|
||||
private bool _hasLastMonthIncome;
|
||||
|
||||
public int MaxChartWidth => SpendingByCategoryChartData.Count * 150;
|
||||
|
||||
public string MonthlyIncomeChangeFormatted => _monthlyIncomeChange >= 0
|
||||
? "↑" + _monthlyIncomeChange.ToString("0.0%")
|
||||
: "↓" + _monthlyIncomeChange.ToString("0.0%");
|
||||
public string MonthlyIncomeChangeFormatted
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_hasLastMonthIncome)
|
||||
return MonthlyIncome > 0 ? "NEW" : "—";
|
||||
return _monthlyIncomeChange >= 0
|
||||
? "↑ " + _monthlyIncomeChange.ToString("0.0%")
|
||||
: "↓ " + _monthlyIncomeChange.ToString("0.0%");
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty] private decimal _monthlyExpenses;
|
||||
private decimal _monthlyExpensesChange;
|
||||
private bool _hasLastMonthExpenses;
|
||||
|
||||
public string MonthlyExpenseChangeFormatted => _monthlyExpensesChange >= 0
|
||||
? "↑" + _monthlyExpensesChange.ToString("0.0%")
|
||||
: "↓" + _monthlyExpensesChange.ToString("0.0%");
|
||||
public string MonthlyExpenseChangeFormatted
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_hasLastMonthExpenses)
|
||||
return MonthlyExpenses > 0 ? "NEW" : "—";
|
||||
return _monthlyExpensesChange >= 0
|
||||
? "↑ " + _monthlyExpensesChange.ToString("0.0%")
|
||||
: "↓ " + _monthlyExpensesChange.ToString("0.0%");
|
||||
}
|
||||
}
|
||||
|
||||
public string AccountsSubtitle =>
|
||||
AccountsSummaryData.Count == 1 ? $" {AccountsSummaryData.Count} linked Account" : $"{AccountsSummaryData.Count} linked Accounts";
|
||||
@@ -61,6 +77,8 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
};
|
||||
|
||||
[ObservableProperty] private string _selectedChartTimePeriod = "This Month";
|
||||
[ObservableProperty] private string _selectedChartTimPeriodSubTitle = DateTime.Now.ToString("MMMM yyyy");
|
||||
[ObservableProperty] private string _dateToday = DateTime.Now.ToString("dddd, MMMM d, yyyy");
|
||||
|
||||
partial void OnSelectedChartTimePeriodChanged(string value)
|
||||
{
|
||||
@@ -73,11 +91,25 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
_ => ChartTimePeriod.ThisMonth
|
||||
};
|
||||
|
||||
SelectedChartTimPeriodSubTitle = value switch
|
||||
{
|
||||
"This Month" => DateTime.Now.ToString("MMMM yyyy"),
|
||||
"Last Month" => DateTime.Now.AddMonths(-1).ToString("MMMM yyyy"),
|
||||
"This Quarter" => $"Q{(DateTime.Now.Month - 1) / 3 + 1} {DateTime.Now.Year}",
|
||||
"This Year" => DateTime.Now.Year.ToString(),
|
||||
_ => DateTime.Now.ToString("MMMM yyyy")
|
||||
};
|
||||
|
||||
UpdateSpendingByCategoryChart(period);
|
||||
}
|
||||
|
||||
public DashboardViewModel()
|
||||
{
|
||||
AppData.Transactions.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
AppData.Accounts.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
AppData.Categories.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
AppData.Budgets.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
initialize();
|
||||
}
|
||||
|
||||
public void initialize()
|
||||
@@ -88,36 +120,44 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void UpdateUserOverview()
|
||||
{
|
||||
var thisMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
|
||||
var lastMonth = thisMonth.AddMonths(-1);
|
||||
|
||||
MonthlyIncome = Transactions.Where(x => x.Type == "income" && x.Date.Month == thisMonth.Month && x.Date.Year == thisMonth.Year)
|
||||
.Sum(x => x.Amount);
|
||||
MonthlyExpenses = Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month && x.Date.Year == DateTime.Now.Year)
|
||||
.Sum(x => x.Amount);
|
||||
var lastMonthIncome = Transactions.Where(x => x.Type == "income" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
|
||||
.Sum(x => x.Amount);
|
||||
var lastMonthExpenses = Transactions.Where(x => x.Type == "expense" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
|
||||
.Sum(x => x.Amount);
|
||||
try
|
||||
{
|
||||
_monthlyIncomeChange = Math.Round((MonthlyIncome / ((lastMonthIncome == 0) ? 1 : lastMonthIncome)) - 1, 2);
|
||||
_monthlyExpensesChange = Math.Round((MonthlyExpenses / ((lastMonthExpenses == 0) ? 1 : lastMonthExpenses)) - 1, 2);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(MonthlyIncomeChangeFormatted));
|
||||
OnPropertyChanged(nameof(MonthlyExpenseChangeFormatted));
|
||||
|
||||
CalculateMonthlyValues();
|
||||
UpdateSpendingByCategoryChart();
|
||||
_ = UpdateBudgetTracker();
|
||||
UpdateRecentTransactions();
|
||||
UpdateAccountsSummary();
|
||||
}
|
||||
|
||||
private void CalculateMonthlyValues()
|
||||
{
|
||||
var thisMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
|
||||
var lastMonth = thisMonth.AddMonths(-1);
|
||||
|
||||
MonthlyIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == thisMonth.Month && x.Date.Year == thisMonth.Year)
|
||||
.Sum(x => x.Amount);
|
||||
MonthlyExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month && x.Date.Year == DateTime.Now.Year)
|
||||
.Sum(x => x.Amount);
|
||||
var lastMonthIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
|
||||
.Sum(x => x.Amount);
|
||||
var lastMonthExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
|
||||
.Sum(x => x.Amount);
|
||||
|
||||
_hasLastMonthIncome = lastMonthIncome > 0;
|
||||
_hasLastMonthExpenses = lastMonthExpenses > 0;
|
||||
|
||||
if (_hasLastMonthIncome)
|
||||
{
|
||||
_monthlyIncomeChange = Math.Round((MonthlyIncome / lastMonthIncome) - 1, 2);
|
||||
}
|
||||
|
||||
if (_hasLastMonthExpenses)
|
||||
{
|
||||
_monthlyExpensesChange = Math.Round((MonthlyExpenses / lastMonthExpenses) - 1, 2);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(MonthlyIncomeChangeFormatted));
|
||||
OnPropertyChanged(nameof(MonthlyExpenseChangeFormatted));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ViewAllTransactions()
|
||||
{
|
||||
@@ -137,10 +177,10 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
{
|
||||
var tempList = new List<ColumnChartData>();
|
||||
|
||||
foreach (var category in Categories)
|
||||
foreach (var category in AppData.Categories)
|
||||
{
|
||||
var categoryTransactions =
|
||||
Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase));
|
||||
AppData.Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
switch (period)
|
||||
{
|
||||
@@ -196,20 +236,20 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
|
||||
private void UpdateRecentTransactions()
|
||||
{
|
||||
RecentTransactions = new ObservableCollection<Transaction>(Transactions.OrderByDescending(x => x.Date).Take(5));
|
||||
RecentTransactions = new ObservableCollection<Transaction>(AppData.Transactions.OrderByDescending(x => x.Date).Take(5));
|
||||
OnPropertyChanged(nameof(HasTransactionData));
|
||||
}
|
||||
|
||||
private void UpdateAccountsSummary()
|
||||
{
|
||||
foreach (var account in Accounts)
|
||||
foreach (var account in AppData.Accounts)
|
||||
{
|
||||
var accountTransactions = Transactions.Where(t => t.AccountId == account.Id).ToList();
|
||||
var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
|
||||
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
|
||||
TotalNetworth += account.CurrentBalance;
|
||||
}
|
||||
|
||||
AccountsSummaryData = new ObservableCollection<Account>(Accounts.OrderBy(x => x.CreatedAt));
|
||||
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.OrderBy(x => x.CreatedAt));
|
||||
OnPropertyChanged(nameof(AccountsSubtitle));
|
||||
}
|
||||
|
||||
|
||||
154
Clario/ViewModels/DeleteAccountDialogViewModel.cs
Normal file
154
Clario/ViewModels/DeleteAccountDialogViewModel.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class DeleteAccountDialogViewModel : ViewModelBase
|
||||
{
|
||||
// ── State machine ────────────────────────────────────────
|
||||
public enum DialogStep
|
||||
{
|
||||
SimpleConfirm,
|
||||
HasTransactions,
|
||||
Migrate
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(
|
||||
nameof(IsSimpleConfirmStep),
|
||||
nameof(IsHasTransactionsStep),
|
||||
nameof(IsMigrateStep))]
|
||||
private DialogStep _currentStep;
|
||||
|
||||
public bool IsSimpleConfirmStep => CurrentStep == DialogStep.SimpleConfirm;
|
||||
public bool IsHasTransactionsStep => CurrentStep == DialogStep.HasTransactions;
|
||||
public bool IsMigrateStep => CurrentStep == DialogStep.Migrate;
|
||||
|
||||
// ── Data ─────────────────────────────────────────────────
|
||||
[ObservableProperty] private Account? _account;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(CanMigrateAndDelete))]
|
||||
private Account? _targetAccount;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Account> _availableAccounts = new();
|
||||
|
||||
// ── Validation ───────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
|
||||
public bool CanMigrateAndDelete =>
|
||||
TargetAccount is not null &&
|
||||
TargetAccount.Id != Account?.Id;
|
||||
|
||||
// ── Callbacks ────────────────────────────────────────────
|
||||
public Action? OnDeleted;
|
||||
public Action? OnCancelled;
|
||||
|
||||
// ── Setup ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Call this to open the dialog for a specific account.
|
||||
/// Automatically determines whether to show simple confirm or migrate warning.
|
||||
/// </summary>
|
||||
public void Setup(Account account, ObservableCollection<Account> allAccounts)
|
||||
{
|
||||
Account = account;
|
||||
ErrorMessage = null;
|
||||
|
||||
// filter out the account being deleted from target options
|
||||
var others = allAccounts
|
||||
.Where(a => a.Id != account.Id && !a.GroupHeader)
|
||||
.ToList();
|
||||
|
||||
AvailableAccounts = new ObservableCollection<Account>(others);
|
||||
TargetAccount = AvailableAccounts.FirstOrDefault();
|
||||
|
||||
// decide which step to show based on transaction count
|
||||
CurrentStep = account.TransactionsCount > 0
|
||||
? DialogStep.HasTransactions
|
||||
: DialogStep.SimpleConfirm;
|
||||
}
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel() => OnCancelled?.Invoke();
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToMigrateStep()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
CurrentStep = DialogStep.Migrate;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void BackToWarning()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
CurrentStep = DialogStep.HasTransactions;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmDelete()
|
||||
{
|
||||
if (Account is null) return;
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
await DataRepo.General.DeleteAccount(Account.Id);
|
||||
OnDeleted?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to delete account. Please try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task MigrateAndDelete()
|
||||
{
|
||||
if (Account is null || TargetAccount is null)
|
||||
{
|
||||
ErrorMessage = "Please select a target account.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (TargetAccount.Id == Account.Id)
|
||||
{
|
||||
ErrorMessage = "Target account must be different from the account being deleted.";
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. re-link all transactions from deleted account to target
|
||||
await DataRepo.General.MigrateTransactions(Account.Id, TargetAccount.Id);
|
||||
|
||||
// 2. recalculate balances on both accounts
|
||||
await DataRepo.General.RecalculateAccountBalance(TargetAccount.Id);
|
||||
|
||||
// 3. delete the account
|
||||
await DataRepo.General.DeleteAccount(Account.Id);
|
||||
|
||||
OnDeleted?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Migration failed. Please try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -12,26 +11,33 @@ using Clario.Models.GeneralModels;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
private DashboardViewModel _dashboardViewModel;
|
||||
public TransactionsViewModel _transactionsViewModel;
|
||||
private AccountsViewModel _accountsViewModel;
|
||||
private BudgetViewModel _budgetViewModel;
|
||||
[ObservableProperty] private TransactionFormViewModel _transactionFormViewModel;
|
||||
[ObservableProperty] public Profile? _profile;
|
||||
private List<Transaction> _transactions = new();
|
||||
private List<Category> _categories = new();
|
||||
private List<Budget> _budgets = new();
|
||||
private List<Account> _accounts = new();
|
||||
private DashboardViewModel _dashboardViewModel = null!;
|
||||
public TransactionsViewModel _transactionsViewModel = null!;
|
||||
private AccountsViewModel _accountsViewModel = null!;
|
||||
private BudgetViewModel _budgetViewModel = null!;
|
||||
|
||||
GeneralDataRepo AppData => DataRepo.General;
|
||||
[ObservableProperty] private Profile? _profile;
|
||||
|
||||
[ObservableProperty] private TransactionFormViewModel _transactionFormViewModel = null!;
|
||||
[ObservableProperty] private AccountFormViewModel _accountFormViewModel = null!;
|
||||
[ObservableProperty] private BudgetFormViewModel _budgetFormViewModel = null!;
|
||||
[ObservableProperty] private SettingsViewModel _settingsViewModel = null!;
|
||||
|
||||
[ObservableProperty] private bool _isDimmed;
|
||||
[ObservableProperty] private bool _isTransactionFormVisible;
|
||||
[ObservableProperty] private bool _isAccountFormVisible;
|
||||
[ObservableProperty] private bool _isBudgetFormVisible;
|
||||
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget))]
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnSettings))]
|
||||
private ViewModelBase? _currentView;
|
||||
|
||||
[ObservableProperty] private bool _isDarkTheme;
|
||||
@@ -39,6 +45,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
public MainViewModel()
|
||||
{
|
||||
Console.WriteLine("main vm loaded");
|
||||
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (_, m) => { Profile = AppData.Profile; });
|
||||
CurrentView = new LoadingViewModel();
|
||||
_ = InitializeApp();
|
||||
}
|
||||
@@ -48,72 +55,71 @@ public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var profilesTask = DataRepo.General.FetchProfileInfo();
|
||||
var categoriesTask = DataRepo.General.FetchCategories();
|
||||
var accountsTask = DataRepo.General.FetchAccounts();
|
||||
var transactionsTask = DataRepo.General.FetchTransactions();
|
||||
var budgetsTask = DataRepo.General.FetchBudgets();
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var profilesTask = DataRepo.General.FetchProfileInfo(forceRefresh: true);
|
||||
var categoriesTask = DataRepo.General.FetchCategories();
|
||||
var transactionsTask = DataRepo.General.FetchTransactions();
|
||||
var accountsTask = DataRepo.General.FetchAccounts();
|
||||
var budgetsTask = DataRepo.General.FetchBudgets();
|
||||
|
||||
await Task.WhenAll(profilesTask, categoriesTask, accountsTask, transactionsTask, budgetsTask);
|
||||
await Task.WhenAll(profilesTask, categoriesTask, accountsTask, transactionsTask, budgetsTask);
|
||||
|
||||
Profile = profilesTask.Result;
|
||||
_categories = categoriesTask.Result;
|
||||
_accounts = accountsTask.Result;
|
||||
_transactions = transactionsTask.Result;
|
||||
_budgets = budgetsTask.Result;
|
||||
Profile = profilesTask.Result;
|
||||
|
||||
Console.WriteLine("fetched all data");
|
||||
DataRepo.General.LinkTransactionCategories();
|
||||
|
||||
Console.WriteLine("fetched all data");
|
||||
});
|
||||
|
||||
_dashboardViewModel = new DashboardViewModel()
|
||||
{
|
||||
parentViewModel = this,
|
||||
Transactions = _transactions,
|
||||
Categories = _categories,
|
||||
Accounts = _accounts,
|
||||
Budgets = _budgets
|
||||
parentViewModel = this
|
||||
};
|
||||
_dashboardViewModel.initialize();
|
||||
CurrentView = _dashboardViewModel;
|
||||
|
||||
Console.WriteLine("initialized DashboardViewModel");
|
||||
_transactionsViewModel = new TransactionsViewModel()
|
||||
{
|
||||
parentViewModel = this,
|
||||
AllTransactions = _transactions.OrderByDescending(x => x.Date).ToList(),
|
||||
Categories = new ObservableCollection<Category>(_categories.OrderBy(x => x.CreatedAt)),
|
||||
Accounts = new ObservableCollection<Account>(_accounts.OrderBy(x => x.CreatedAt))
|
||||
parentViewModel = this
|
||||
};
|
||||
await _transactionsViewModel.Initialize();
|
||||
|
||||
Console.WriteLine("initialized TransactionsViewModel");
|
||||
_accountsViewModel = new AccountsViewModel()
|
||||
{
|
||||
parentViewModel = this,
|
||||
Accounts = _accounts,
|
||||
Transactions = _transactions
|
||||
parentViewModel = this
|
||||
};
|
||||
await _accountsViewModel.Initialize();
|
||||
|
||||
Console.WriteLine("initialized AccountsViewModel");
|
||||
_budgetViewModel = new BudgetViewModel()
|
||||
{
|
||||
parentViewModel = this,
|
||||
Profile = Profile,
|
||||
Budgets = _budgets,
|
||||
Categories = _categories,
|
||||
Transactions = _transactions
|
||||
parentViewModel = this
|
||||
};
|
||||
await _budgetViewModel.Initialize();
|
||||
Console.WriteLine("initialized BudgetViewModel");
|
||||
SettingsViewModel = new SettingsViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
Console.WriteLine("initialized SettingsViewModel");
|
||||
TransactionFormViewModel = new TransactionFormViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
Console.WriteLine("initialized TransactionFormViewModel");
|
||||
AccountFormViewModel = new AccountFormViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
Console.WriteLine("initialized AccountFormViewModel");
|
||||
BudgetFormViewModel = new BudgetFormViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
Console.WriteLine("initialized BudgetFormViewModel");
|
||||
|
||||
IsDarkTheme = ThemeService.IsDarkTheme;
|
||||
|
||||
ThemeService.SwitchToTheme(Profile?.Theme ?? "system");
|
||||
ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -124,42 +130,15 @@ public partial class MainViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
public void OpenAddTransaction()
|
||||
{
|
||||
if (IsTransactionFormVisible) return;
|
||||
if (IsDimmed) return;
|
||||
try
|
||||
{
|
||||
TransactionFormViewModel.SetupForAdd(
|
||||
new ObservableCollection<Category>(_categories),
|
||||
new ObservableCollection<Account>(_accounts)
|
||||
);
|
||||
TransactionFormViewModel.OnSaved = () =>
|
||||
{
|
||||
if (TransactionFormViewModel.ResultTransaction is not null)
|
||||
{
|
||||
var previousItem = _transactionsViewModel.AllTransactions.FirstOrDefault(x => x.Date < TransactionFormViewModel.ResultTransaction.Date);
|
||||
var index = 0;
|
||||
if (previousItem is not null)
|
||||
index = _transactionsViewModel.AllTransactions.IndexOf(previousItem);
|
||||
if (index == -1) index = 0;
|
||||
_transactionsViewModel.AllTransactions.Insert(index, TransactionFormViewModel.ResultTransaction);
|
||||
_dashboardViewModel.Transactions.Insert(index, TransactionFormViewModel.ResultTransaction);
|
||||
_dashboardViewModel.UpdateUserOverviewCommand.Execute(null);
|
||||
_transactionsViewModel.LoadPageCommand.Execute(1);
|
||||
}
|
||||
|
||||
CloseTransactionForm();
|
||||
};
|
||||
TransactionFormViewModel.OnCancelled = () => CloseTransactionForm();
|
||||
TransactionFormViewModel.OnDeleted = () =>
|
||||
{
|
||||
if (TransactionFormViewModel.ResultTransaction is { } resultTransaction)
|
||||
{
|
||||
_transactionsViewModel.AllTransactions.Remove(resultTransaction);
|
||||
_transactionsViewModel.LoadPageCommand.Execute(1);
|
||||
}
|
||||
|
||||
CloseTransactionForm();
|
||||
};
|
||||
TransactionFormViewModel.SetupForAdd();
|
||||
TransactionFormViewModel.OnSaved = CloseTransactionForm;
|
||||
TransactionFormViewModel.OnCancelled = CloseTransactionForm;
|
||||
TransactionFormViewModel.OnDeleted = CloseTransactionForm;
|
||||
IsTransactionFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -171,41 +150,111 @@ public partial class MainViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
public void OpenEditTransaction(Transaction transaction)
|
||||
{
|
||||
TransactionFormViewModel.SetupForEdit(
|
||||
transaction,
|
||||
new ObservableCollection<Category>(_categories),
|
||||
new ObservableCollection<Account>(_accounts)
|
||||
);
|
||||
TransactionFormViewModel.OnSaved = () =>
|
||||
{
|
||||
if (TransactionFormViewModel.ResultTransaction is { } resultTransaction)
|
||||
{
|
||||
var index = _transactionsViewModel.AllTransactions.FindIndex(x => x.Id == transaction.Id);
|
||||
if (index != -1)
|
||||
_transactionsViewModel.AllTransactions[index] = resultTransaction;
|
||||
_transactionsViewModel.LoadPageCommand.Execute(1);
|
||||
}
|
||||
|
||||
CloseTransactionForm();
|
||||
};
|
||||
if (IsDimmed) return;
|
||||
TransactionFormViewModel.SetupForEdit(transaction);
|
||||
TransactionFormViewModel.OnSaved = CloseTransactionForm;
|
||||
TransactionFormViewModel.OnCancelled = CloseTransactionForm;
|
||||
TransactionFormViewModel.OnDeleted = () =>
|
||||
{
|
||||
if (TransactionFormViewModel.ResultTransaction is { } resultTransaction)
|
||||
{
|
||||
_transactionsViewModel.AllTransactions.Remove(resultTransaction);
|
||||
_transactionsViewModel.LoadPageCommand.Execute(1);
|
||||
}
|
||||
|
||||
CloseTransactionForm();
|
||||
};
|
||||
TransactionFormViewModel.OnDeleted = CloseTransactionForm;
|
||||
IsTransactionFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
|
||||
|
||||
private void CloseTransactionForm()
|
||||
{
|
||||
IsTransactionFormVisible = false;
|
||||
IsDimmed = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenAddAccount()
|
||||
{
|
||||
if (IsDimmed) return;
|
||||
try
|
||||
{
|
||||
AccountFormViewModel.SetupForAdd();
|
||||
AccountFormViewModel.OnSaved = CloseAccountForm;
|
||||
AccountFormViewModel.OnCancelled = CloseAccountForm;
|
||||
IsAccountFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenEditAccount(Account account)
|
||||
{
|
||||
if (IsDimmed) return;
|
||||
try
|
||||
{
|
||||
AccountFormViewModel.SetupForEdit(account);
|
||||
AccountFormViewModel.OnSaved = CloseAccountForm;
|
||||
AccountFormViewModel.OnCancelled = CloseAccountForm;
|
||||
IsAccountFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseAccountForm()
|
||||
{
|
||||
IsAccountFormVisible = false;
|
||||
IsDimmed = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenAddBudget()
|
||||
{
|
||||
if (IsDimmed) return;
|
||||
try
|
||||
{
|
||||
var unusedCategories = AppData.Categories.Where(x => AppData.Budgets.All(y => y.Category?.Id != x.Id)).ToList();
|
||||
BudgetFormViewModel.SetupForAdd(new ObservableCollection<Category>(unusedCategories));
|
||||
BudgetFormViewModel.OnSaved = CloseBudgetForm;
|
||||
BudgetFormViewModel.OnCancelled = CloseBudgetForm;
|
||||
BudgetFormViewModel.OnDeleted = CloseBudgetForm;
|
||||
IsBudgetFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenEditBudget(Budget budget)
|
||||
{
|
||||
if (IsDimmed) return;
|
||||
try
|
||||
{
|
||||
var unusedCategories = AppData.Categories.Where(x => AppData.Budgets.All(y => y.Category?.Id != x.Id) || x.Id == budget.CategoryId).ToList();
|
||||
BudgetFormViewModel.SetupForEdit(budget, new ObservableCollection<Category>(unusedCategories));
|
||||
BudgetFormViewModel.OnSaved = CloseBudgetForm;
|
||||
BudgetFormViewModel.OnCancelled = CloseBudgetForm;
|
||||
BudgetFormViewModel.OnDeleted = CloseBudgetForm;
|
||||
IsBudgetFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseBudgetForm()
|
||||
{
|
||||
IsDimmed = false;
|
||||
IsBudgetFormVisible = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -239,13 +288,19 @@ public partial class MainViewModel : ViewModelBase
|
||||
CurrentView = _budgetViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToSettings()
|
||||
{
|
||||
CurrentView = _settingsViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SignOut()
|
||||
{
|
||||
await SupabaseService.Client.Auth.SignOut();
|
||||
var user = SupabaseService.Client.Auth.CurrentUser;
|
||||
|
||||
switch (Application.Current.ApplicationLifetime)
|
||||
switch (Application.Current?.ApplicationLifetime)
|
||||
{
|
||||
case IClassicDesktopStyleApplicationLifetime desktop:
|
||||
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
@@ -260,4 +315,5 @@ public partial class MainViewModel : ViewModelBase
|
||||
public bool isOnTransactions => CurrentView is TransactionsViewModel;
|
||||
public bool isOnAccounts => CurrentView is AccountsViewModel;
|
||||
public bool isOnBudget => CurrentView is BudgetViewModel;
|
||||
public bool isOnSettings => CurrentView is SettingsViewModel;
|
||||
}
|
||||
418
Clario/ViewModels/SettingsViewModel.cs
Normal file
418
Clario/ViewModels/SettingsViewModel.cs
Normal file
@@ -0,0 +1,418 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using Clario.Models.GeneralModels;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class SettingsViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
public static readonly HttpClient _HttpClient = new();
|
||||
|
||||
|
||||
// ── Profile fields ───────────────────────────────────────
|
||||
[ObservableProperty] private string _displayName = "";
|
||||
[ObservableProperty] private string _avatarUrl = "";
|
||||
[ObservableProperty] private Bitmap? _avatarImage;
|
||||
[ObservableProperty] private string _selectedCurrency = "USD";
|
||||
[ObservableProperty] private string _selectedTheme = "system";
|
||||
[ObservableProperty] private string _selectedLanguage = "en";
|
||||
|
||||
// ── Account (auth) fields ────────────────────────────────
|
||||
[ObservableProperty] private string _maskedEmail = "";
|
||||
private string _fullEmail = "";
|
||||
|
||||
// ── Change email flow ────────────────────────────────────
|
||||
[ObservableProperty] private bool _isChangingEmail = false;
|
||||
[ObservableProperty] private string _newEmail = "";
|
||||
[ObservableProperty] private string _emailConfirmPassword = "";
|
||||
|
||||
// ── Change password flow ─────────────────────────────────
|
||||
[ObservableProperty] private bool _isChangingPassword = false;
|
||||
[ObservableProperty] private string _currentPassword = "";
|
||||
[ObservableProperty] private string _newPassword = "";
|
||||
[ObservableProperty] private string _confirmNewPassword = "";
|
||||
|
||||
// ── UI state ─────────────────────────────────────────────
|
||||
[ObservableProperty] private bool _isSaving = false;
|
||||
[ObservableProperty] private bool _isUploadingAvatar = false;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasSuccess))]
|
||||
private string? _successMessage;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasEmailSuccess))]
|
||||
private string? _emailSuccessMessage;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasEmailError))]
|
||||
private string? _emailErrorMessage;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasPasswordSuccess))]
|
||||
private string? _passwordSuccessMessage;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasPasswordError))]
|
||||
private string? _passwordErrorMessage;
|
||||
|
||||
public bool HasSuccess => !string.IsNullOrEmpty(SuccessMessage);
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
public bool HasEmailSuccess => !string.IsNullOrEmpty(EmailSuccessMessage);
|
||||
public bool HasEmailError => !string.IsNullOrEmpty(EmailErrorMessage);
|
||||
public bool HasPasswordSuccess => !string.IsNullOrEmpty(PasswordSuccessMessage);
|
||||
public bool HasPasswordError => !string.IsNullOrEmpty(PasswordErrorMessage);
|
||||
public bool HasAvatar => !string.IsNullOrEmpty(AvatarUrl);
|
||||
|
||||
// ── Options ──────────────────────────────────────────────
|
||||
public ObservableCollection<string> Currencies { get; } = new()
|
||||
{
|
||||
"USD", "EUR", "GBP", "JPY", "AED", "SAR", "JOD",
|
||||
"EGP", "CAD", "AUD", "CHF", "CNY", "INR", "BRL"
|
||||
};
|
||||
|
||||
public ObservableCollection<(string Value, string Label)> Themes { get; } = new()
|
||||
{
|
||||
("system", "System default"),
|
||||
("dark", "Dark"),
|
||||
("light", "Light")
|
||||
};
|
||||
|
||||
public ObservableCollection<string> ThemeLabels { get; } = new()
|
||||
{
|
||||
"System default", "Dark", "Light"
|
||||
};
|
||||
|
||||
public ObservableCollection<(string Value, string Label)> Languages { get; } = new()
|
||||
{
|
||||
("en", "English"),
|
||||
("ar", "العربية"),
|
||||
};
|
||||
|
||||
public ObservableCollection<string> LanguageLabels { get; } = new()
|
||||
{
|
||||
"English", "العربية"
|
||||
};
|
||||
|
||||
// ComboBox selected indices (mapped to/from string values)
|
||||
[ObservableProperty] private int _selectedThemeIndex = 0;
|
||||
[ObservableProperty] private int _selectedLanguageIndex = 0;
|
||||
|
||||
partial void OnSelectedThemeIndexChanged(int value)
|
||||
{
|
||||
SelectedTheme = value switch { 0 => "system", 1 => "dark", 2 => "light", _ => "system" };
|
||||
}
|
||||
|
||||
partial void OnSelectedLanguageIndexChanged(int value)
|
||||
{
|
||||
SelectedLanguage = value switch { 0 => "en", 1 => "ar", _ => "en" };
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────
|
||||
public SettingsViewModel()
|
||||
{
|
||||
_ = Initialize();
|
||||
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, async (_, m) => { await Initialize(); });
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
DisplayName = AppData.Profile?.DisplayName ?? "";
|
||||
AvatarUrl = DataRepo.General.BuildPublicUrl(AppData.Profile?.AvatarUrl) ?? "";
|
||||
AvatarImage = AppData.Profile?.Avatar;
|
||||
SelectedCurrency = AppData.Profile?.Currency ?? "USD";
|
||||
SelectedTheme = AppData.Profile?.Theme ?? "system";
|
||||
SelectedLanguage = AppData.Profile?.Language ?? "en";
|
||||
|
||||
// sync indices
|
||||
SelectedThemeIndex = SelectedTheme switch { "dark" => 1, "light" => 2, _ => 0 };
|
||||
SelectedLanguageIndex = SelectedLanguage switch { "ar" => 1, _ => 0 };
|
||||
|
||||
// mask email
|
||||
_fullEmail = SupabaseService.Client.Auth.CurrentUser?.Email ?? "";
|
||||
MaskedEmail = MaskEmail(_fullEmail);
|
||||
}
|
||||
|
||||
|
||||
private static string MaskEmail(string email)
|
||||
{
|
||||
if (string.IsNullOrEmpty(email)) return "";
|
||||
var atIndex = email.IndexOf('@');
|
||||
if (atIndex <= 2) return email; // too short to mask
|
||||
var local = email[..atIndex];
|
||||
var domain = email[atIndex..];
|
||||
var visible = local[..2];
|
||||
var masked = new string('•', Math.Min(local.Length - 2, 5));
|
||||
return $"{visible}{masked}{domain}";
|
||||
}
|
||||
|
||||
// ── Avatar commands ───────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private async Task UploadAvatar()
|
||||
{
|
||||
var file = await FilePickerService.Instance.PickImageAsync();
|
||||
if (file is null) return;
|
||||
|
||||
IsUploadingAvatar = true;
|
||||
ErrorMessage = null;
|
||||
SuccessMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var localPath = file.Path.LocalPath;
|
||||
var url = await DataRepo.General.UploadAvatarAsync(localPath);
|
||||
AvatarUrl = url;
|
||||
|
||||
// persist to profile
|
||||
await DataRepo.General.UpdateProfileAvatar(url);
|
||||
SuccessMessage = "Avatar updated successfully.";
|
||||
await Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to upload avatar. Please try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsUploadingAvatar = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RemoveAvatar()
|
||||
{
|
||||
IsUploadingAvatar = true;
|
||||
ErrorMessage = null;
|
||||
SuccessMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
await DataRepo.General.DeleteAvatarAsync();
|
||||
await DataRepo.General.UpdateProfileAvatar(null);
|
||||
AvatarUrl = "";
|
||||
SuccessMessage = "Avatar removed.";
|
||||
await Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to remove avatar.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsUploadingAvatar = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save profile ─────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveProfile()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DisplayName))
|
||||
{
|
||||
ErrorMessage = "Display name cannot be empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
SuccessMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var updated = new Profile
|
||||
{
|
||||
Id = AppData.Profile.Id,
|
||||
DisplayName = DisplayName.Trim(),
|
||||
Currency = SelectedCurrency,
|
||||
Theme = SelectedTheme,
|
||||
Language = SelectedLanguage,
|
||||
AvatarUrl = AppData.Profile.AvatarUrl,
|
||||
Avatar = AppData.Profile.Avatar,
|
||||
SavingsGoal = AppData.Profile.SavingsGoal
|
||||
};
|
||||
|
||||
await DataRepo.General.UpdateProfile(updated);
|
||||
|
||||
// apply theme immediately
|
||||
ThemeService.SwitchToTheme(SelectedTheme);
|
||||
|
||||
SuccessMessage = "Profile saved successfully.";
|
||||
await Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to save profile. Please try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Change email ─────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void StartChangeEmail()
|
||||
{
|
||||
NewEmail = "";
|
||||
EmailConfirmPassword = "";
|
||||
EmailErrorMessage = null;
|
||||
EmailSuccessMessage = null;
|
||||
IsChangingEmail = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelChangeEmail()
|
||||
{
|
||||
IsChangingEmail = false;
|
||||
EmailErrorMessage = null;
|
||||
EmailSuccessMessage = null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmChangeEmail()
|
||||
{
|
||||
EmailErrorMessage = null;
|
||||
EmailSuccessMessage = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(NewEmail) || !NewEmail.Contains('@'))
|
||||
{
|
||||
EmailErrorMessage = "Please enter a valid email address.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(EmailConfirmPassword))
|
||||
{
|
||||
EmailErrorMessage = "Please enter your current password to confirm.";
|
||||
return;
|
||||
}
|
||||
|
||||
IsSaving = true;
|
||||
try
|
||||
{
|
||||
// re-authenticate first to confirm password
|
||||
await SupabaseService.Client.Auth.SignIn(_fullEmail, EmailConfirmPassword);
|
||||
|
||||
// update email — Supabase sends confirmation to the new address
|
||||
await SupabaseService.Client.Auth.Update(new Supabase.Gotrue.UserAttributes
|
||||
{
|
||||
Email = NewEmail.Trim()
|
||||
});
|
||||
|
||||
EmailSuccessMessage = "Confirmation sent to your new email address. Please check your inbox.";
|
||||
IsChangingEmail = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmailErrorMessage = "Failed to update email. Check your password and try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Change password ──────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void StartChangePassword()
|
||||
{
|
||||
CurrentPassword = "";
|
||||
NewPassword = "";
|
||||
ConfirmNewPassword = "";
|
||||
PasswordErrorMessage = null;
|
||||
PasswordSuccessMessage = null;
|
||||
IsChangingPassword = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelChangePassword()
|
||||
{
|
||||
IsChangingPassword = false;
|
||||
PasswordErrorMessage = null;
|
||||
PasswordSuccessMessage = null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmChangePassword()
|
||||
{
|
||||
PasswordErrorMessage = null;
|
||||
PasswordSuccessMessage = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CurrentPassword))
|
||||
{
|
||||
PasswordErrorMessage = "Please enter your current password.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(NewPassword) || NewPassword.Length < 8)
|
||||
{
|
||||
PasswordErrorMessage = "New password must be at least 8 characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (NewPassword != ConfirmNewPassword)
|
||||
{
|
||||
PasswordErrorMessage = "Passwords do not match.";
|
||||
return;
|
||||
}
|
||||
|
||||
IsSaving = true;
|
||||
try
|
||||
{
|
||||
// re-authenticate to confirm current password
|
||||
await SupabaseService.Client.Auth.SignIn(_fullEmail, CurrentPassword);
|
||||
|
||||
await SupabaseService.Client.Auth.Update(new Supabase.Gotrue.UserAttributes
|
||||
{
|
||||
Password = NewPassword
|
||||
});
|
||||
|
||||
PasswordSuccessMessage = "Password updated successfully.";
|
||||
IsChangingPassword = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PasswordErrorMessage = "Failed to update password. Check your current password and try again.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sign out ─────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SignOut()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ((MainViewModel)parentViewModel).SignOutCommand.ExecuteAsync(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to sign out.";
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace Clario.ViewModels;
|
||||
public partial class TransactionFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
|
||||
@@ -34,6 +35,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private string? _note;
|
||||
[ObservableProperty] private List<DateTime> _dates = [DateTime.Now];
|
||||
[ObservableProperty] private DateTime? _selectedDate;
|
||||
[ObservableProperty] private string _currency = "USD";
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
@@ -75,7 +77,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
public Transaction? ResultTransaction { get; set; }
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
|
||||
|
||||
partial void OnSelectedCategoryChanged(Category? value)
|
||||
{
|
||||
if (value.Type == Type) return;
|
||||
@@ -215,47 +217,43 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
|
||||
/// <summary>Call this to open the form for adding a new transaction.</summary>
|
||||
public void SetupForAdd(
|
||||
ObservableCollection<Category> categories,
|
||||
ObservableCollection<Account> accounts)
|
||||
public void SetupForAdd()
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
IsEditMode = false;
|
||||
_editingId = null;
|
||||
Categories = categories;
|
||||
Accounts = accounts;
|
||||
Categories = AppData.Categories;
|
||||
Accounts = AppData.Accounts;
|
||||
Type = "expense";
|
||||
Amount = "";
|
||||
Description = "";
|
||||
Note = null;
|
||||
Dates = [DateTime.Now];
|
||||
ErrorMessage = null;
|
||||
SelectedCategory = categories.Count > 0 ? categories[0] : null;
|
||||
SelectedAccount = accounts.Count > 0 ? accounts[0] : null;
|
||||
SelectedCategory = AppData.Categories.Count > 0 ? AppData.Categories[0] : null;
|
||||
SelectedAccount = AppData.Accounts.Count > 0 ? AppData.Accounts[0] : null;
|
||||
ResultTransaction = null;
|
||||
}
|
||||
|
||||
/// <summary>Call this to open the form for editing an existing transaction.</summary>
|
||||
public void SetupForEdit(
|
||||
Transaction transaction,
|
||||
ObservableCollection<Category> categories,
|
||||
ObservableCollection<Account> accounts)
|
||||
Transaction transaction)
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
IsEditMode = true;
|
||||
_editingId = transaction.Id;
|
||||
Categories = categories;
|
||||
Accounts = accounts;
|
||||
Categories = AppData.Categories;
|
||||
Accounts = AppData.Accounts;
|
||||
Type = transaction.Type;
|
||||
Amount = transaction.Amount.ToString("0.00");
|
||||
Description = transaction.Description;
|
||||
Note = transaction.Note;
|
||||
Dates = [transaction.Date];
|
||||
ErrorMessage = null;
|
||||
SelectedCategory = categories.FirstOrDefault(c => c.Id == transaction.CategoryId)
|
||||
?? (categories.Count > 0 ? categories[0] : null);
|
||||
SelectedAccount = accounts.FirstOrDefault(a => a.Id == transaction.AccountId)
|
||||
?? (accounts.Count > 0 ? accounts[0] : null);
|
||||
SelectedCategory = AppData.Categories.FirstOrDefault(c => c.Id == transaction.CategoryId)
|
||||
?? (AppData.Categories.Count > 0 ? AppData.Categories[0] : null);
|
||||
SelectedAccount = AppData.Accounts.FirstOrDefault(a => a.Id == transaction.AccountId)
|
||||
?? (AppData.Accounts.Count > 0 ? AppData.Accounts[0] : null);
|
||||
ResultTransaction = transaction;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Messages;
|
||||
using Clario.Models;
|
||||
@@ -11,20 +10,22 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
// ReSharper disable PossibleMultipleEnumeration
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class TransactionsViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
public List<Transaction> AllTransactions = new();
|
||||
[ObservableProperty] private ObservableCollection<Category> _categories = new();
|
||||
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
|
||||
|
||||
[ObservableProperty] private List<Transaction> _filteredTransactions = new();
|
||||
|
||||
private int _pageSize = 25;
|
||||
[ObservableProperty] private int _pageSizeIndex = 0;
|
||||
[ObservableProperty] private int _pageSizeIndex;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(TotalPages))] [NotifyCanExecuteChangedFor(nameof(NextPageCommand), nameof(PreviousPageCommand))]
|
||||
private int _currentPage = 1;
|
||||
@@ -56,8 +57,8 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
|
||||
public List<int> PageNumbers { get; set; }
|
||||
[ObservableProperty] private ObservableCollection<int> _visiblePageNumbers = new();
|
||||
public int TotalPages => (int)Math.Ceiling(_filteredTransactions.Count / (double)_pageSize);
|
||||
public bool HasNoTransactions => _filteredTransactions.Count == 0;
|
||||
public int TotalPages => (int)Math.Ceiling(FilteredTransactions.Count / (double)_pageSize);
|
||||
public bool HasNoTransactions => FilteredTransactions.Count == 0;
|
||||
public bool HasNextPage => CurrentPage < TotalPages;
|
||||
public bool HasPreviousPage => CurrentPage > 1;
|
||||
|
||||
@@ -65,6 +66,7 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
[ObservableProperty] private double _totalIncome;
|
||||
[ObservableProperty] private int _expensesCount;
|
||||
[ObservableProperty] private int _incomeCount;
|
||||
[ObservableProperty] private string _dateRangeLabel = "";
|
||||
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private Category _selectedCategory;
|
||||
@@ -84,6 +86,13 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
|
||||
public TransactionsViewModel()
|
||||
{
|
||||
AppData.Transactions.CollectionChanged += (_, _) =>
|
||||
{
|
||||
InitializeCategories();
|
||||
InitializeAccounts();
|
||||
LoadPage(1);
|
||||
};
|
||||
Initialize();
|
||||
}
|
||||
|
||||
partial void OnPageSizeIndexChanged(int value)
|
||||
@@ -121,8 +130,7 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
{
|
||||
ApplyFilters();
|
||||
if (CurrentPage != page) CurrentPage = page;
|
||||
var items = _filteredTransactions
|
||||
.Skip((page - 1) * _pageSize)
|
||||
var items = FilteredTransactions.Skip((page - 1) * _pageSize)
|
||||
.Take(_pageSize);
|
||||
|
||||
OnPropertyChanged(nameof(HasNoTransactions));
|
||||
@@ -130,9 +138,9 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
foreach (var item in items)
|
||||
PagedTransactions.Add(item);
|
||||
PaginationSummaryText =
|
||||
$"Showing {((page - 1) * _pageSize) + 1}-{(Math.Min(page * _pageSize, _filteredTransactions.Count))} of {_filteredTransactions.Count} transactions";
|
||||
$"Showing {((page - 1) * _pageSize) + 1}-{(Math.Min(page * _pageSize, FilteredTransactions.Count))} of {FilteredTransactions.Count} transactions";
|
||||
PageNumbers = Enumerable.Range(1, Math.Min(TotalPages, 5)).ToList();
|
||||
var numbers = GetSurrounding(PageNumbers, page, 5);
|
||||
var numbers = GetSurrounding(PageNumbers, page);
|
||||
VisiblePageNumbers.Clear();
|
||||
foreach (var number in numbers)
|
||||
VisiblePageNumbers.Add(number);
|
||||
@@ -143,43 +151,45 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void ApplyFilters()
|
||||
{
|
||||
// Console.WriteLine($"Search Text: {_searchText}");
|
||||
// Console.WriteLine($"Category: {_selectedCategory.Name}");
|
||||
// Console.WriteLine($"Account: {_selectedAccount.Name}");
|
||||
// Console.WriteLine($"Transaction Type: {_transactionType}");
|
||||
var filtered = AppData.Transactions.Where(x =>
|
||||
x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
||||
|| x.Note!.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
var filtered = AllTransactions.Where(x =>
|
||||
x.Description.Contains(_searchText, StringComparison.OrdinalIgnoreCase)
|
||||
|| x.Note.Contains(_searchText, StringComparison.OrdinalIgnoreCase));
|
||||
var culture = new CultureInfo("en-US");
|
||||
|
||||
switch (SelectedDateRangeOption)
|
||||
{
|
||||
case "All Time":
|
||||
// do nothing
|
||||
DateRangeLabel = "ALL TIME";
|
||||
break;
|
||||
case "Today":
|
||||
filtered = filtered.Where(x => x.Date == DateTime.Now.Date);
|
||||
DateRangeLabel = DateTime.Now.ToString("MMM d, yyyy", culture).ToUpper();
|
||||
break;
|
||||
case "This Week":
|
||||
var startOfWeek = DateTime.Now.Date.AddDays(-(int)DateTime.Now.DayOfWeek);
|
||||
var endOfWeek = startOfWeek.AddDays(6);
|
||||
filtered = filtered.Where(x => x.Date.Date >= startOfWeek && x.Date.Date <= endOfWeek);
|
||||
DateRangeLabel = "THIS WEEK";
|
||||
break;
|
||||
case "This Month":
|
||||
filtered = filtered.Where(x => x.Date.Month == DateTime.Now.Month);
|
||||
DateRangeLabel = DateTime.Now.ToString("MMMM yyyy", culture).ToUpper();
|
||||
break;
|
||||
case "Last Month":
|
||||
var lastMonth = DateTime.Now.AddMonths(-1);
|
||||
filtered = filtered.Where(x => x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year);
|
||||
DateRangeLabel = lastMonth.ToString("MMMM yyyy", culture).ToUpper();
|
||||
break;
|
||||
case "This Quarter":
|
||||
var startOfQuarter = DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3);
|
||||
var endOfQuarter = startOfQuarter.AddMonths(3);
|
||||
filtered = filtered.Where(x => x.Date >= startOfQuarter && x.Date <= endOfQuarter);
|
||||
DateRangeLabel = $"Q{(DateTime.Now.Month - 1) / 3 + 1} {DateTime.Now.Year}";
|
||||
break;
|
||||
case "This Year":
|
||||
filtered = filtered.Where(x => x.Date.Year == DateTime.Now.Year);
|
||||
DateRangeLabel = DateTime.Now.Year.ToString();
|
||||
break;
|
||||
case "Custom Range":
|
||||
if (SelectedDates is not null && SelectedDates.Count > 0)
|
||||
@@ -192,25 +202,35 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
|
||||
var start = ordered.First();
|
||||
var end = ordered.Last();
|
||||
|
||||
Console.WriteLine($"first {SelectedDates.First():d} / last {SelectedDates.Last():d}");
|
||||
|
||||
if (SelectedDates.Count == 1)
|
||||
{
|
||||
filtered = filtered.Where(x => x.Date.Date == start);
|
||||
DateRangeLabel = start.ToString("MMM dd, yyyy", culture).ToUpper();
|
||||
}
|
||||
else
|
||||
{
|
||||
filtered = filtered.Where(x => x.Date.Date >= start && x.Date.Date <= end);
|
||||
DateRangeLabel = $"{start.ToString("MMM dd", culture)} - {end.ToString("MMM dd, yyyy", culture)}".ToUpper();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (_selectedCategory.Name != "All Categories")
|
||||
filtered = filtered.Where(x => x.CategoryId == _selectedCategory.Id);
|
||||
|
||||
if (_selectedAccount.Name != "All Accounts")
|
||||
filtered = filtered.Where(x => x.AccountId == _selectedAccount.Id);
|
||||
// Calculate totals based on date-filtered transactions
|
||||
TotalExpenses = filtered.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.Amount));
|
||||
TotalIncome = filtered.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.Amount));
|
||||
|
||||
if (_transactionType != "all")
|
||||
filtered = filtered.Where(x => x.Type == _transactionType);
|
||||
if (SelectedCategory.Name != "All Categories")
|
||||
filtered = filtered.Where(x => x.CategoryId == SelectedCategory.Id);
|
||||
|
||||
if (SelectedAccount.Name != "All Accounts")
|
||||
filtered = filtered.Where(x => x.AccountId == SelectedAccount.Id);
|
||||
|
||||
if (TransactionType != "all")
|
||||
filtered = filtered.Where(x => x.Type == TransactionType);
|
||||
|
||||
switch (SelectedSortOption)
|
||||
{
|
||||
@@ -297,7 +317,7 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
public void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -320,21 +340,31 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
private void InitializeCategories()
|
||||
{
|
||||
Categories.Insert(0, new Category() { Name = "All Categories" });
|
||||
foreach (var appDataCategory in AppData.Categories)
|
||||
{
|
||||
Categories.Add(appDataCategory);
|
||||
}
|
||||
|
||||
SelectedCategory = Categories.First();
|
||||
}
|
||||
|
||||
private void InitializeAccounts()
|
||||
{
|
||||
Accounts.Insert(0, new Account() { Name = "All Accounts" });
|
||||
foreach (var appDataAccount in AppData.Accounts)
|
||||
{
|
||||
Accounts.Add(appDataAccount);
|
||||
}
|
||||
|
||||
SelectedAccount = Accounts.First();
|
||||
}
|
||||
|
||||
private void CalculateMonthlyFinancials()
|
||||
{
|
||||
TotalExpenses = AllTransactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.Amount));
|
||||
TotalIncome = AllTransactions.Where(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.Amount));
|
||||
ExpensesCount = AllTransactions.Count(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month);
|
||||
IncomeCount = AllTransactions.Count(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month);
|
||||
TotalExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.Amount));
|
||||
TotalIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.Amount));
|
||||
ExpensesCount = AppData.Transactions.Count(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month);
|
||||
IncomeCount = AppData.Transactions.Count(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month);
|
||||
}
|
||||
|
||||
public static List<T> GetSurrounding<T>(List<T> list, T item, int count = 5)
|
||||
@@ -351,13 +381,13 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
|
||||
return list.GetRange(start, end - start);
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateTransaction()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddTransaction();
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void EditTransaction(Transaction transaction)
|
||||
{
|
||||
|
||||
328
Clario/Views/AccountFormView.axaml
Normal file
328
Clario/Views/AccountFormView.axaml
Normal file
@@ -0,0 +1,328 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:cc="clr-namespace:Clario.CustomControls"
|
||||
xmlns:behaviors="clr-namespace:Clario.Behaviors"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Clario.Views.AccountFormView"
|
||||
x:DataType="vm:AccountFormViewModel">
|
||||
<Design.DataContext>
|
||||
<vm:AccountFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<!-- ── Dim overlay ───────────────────────── -->
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- ── Modal card ────────────────────────── -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="460"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Header ──────────────────────── -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
Width="42" Height="42"
|
||||
Margin="0,0,14,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedIcon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="18" Height="18"
|
||||
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding FormTitle}"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="{Binding FormSubtitle}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
VerticalAlignment="Top"
|
||||
Cursor="Hand"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Name ──────────────────────── -->
|
||||
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Name, Mode=TwoWay}"
|
||||
Watermark="e.g. Main Checking"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<!-- ── Type ─────────────────────────── -->
|
||||
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,16">
|
||||
<ComboBox ItemsSource="{Binding AccountTypes}"
|
||||
SelectedItem="{Binding SelectedType, Mode=TwoWay}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="12,10"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Institution + Mask ──────────── -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Institution -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="INSTITUTION (OPTIONAL)" Classes="label" />
|
||||
<TextBox Text="{Binding Institution, Mode=TwoWay}"
|
||||
Watermark="e.g. Chase"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Mask -->
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="LAST 4 DIGITS (OPTIONAL)" Classes="label" />
|
||||
<TextBox Text="{Binding Mask, Mode=TwoWay}"
|
||||
Watermark="e.g. 1234"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
MaxLength="4"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Opening Balance + Currency ──────────── -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Opening Balance -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="OPENING BALANCE" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="12,0">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding OpeningBalance, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="38"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Currency -->
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="CURRENCY" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}">
|
||||
<ComboBox ItemsSource="{Binding Currencies}"
|
||||
SelectedItem="{Binding Currency, Mode=TwoWay}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="12,10"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Credit Limit (if type is credit) ──────────── -->
|
||||
<StackPanel Spacing="6" Margin="0,0,0,16" IsVisible="{Binding IsCredit}">
|
||||
<TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="12,0">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding CreditLimit, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="38"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Opened At ──────────────────────── -->
|
||||
<TextBlock Text="OPENED ON (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,16">
|
||||
<cc:DateRangePicker Classes="ghost"
|
||||
SelectionMode="SingleDate"
|
||||
SelectedDates="{Binding OpenedAtDates}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="12,10" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Icon + Color ──────────── -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Icon -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="ICON" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="30" Height="30"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedIcon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="14" Height="14"
|
||||
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding Icons}"
|
||||
SelectedItem="{Binding SelectedIcon, Mode=TwoWay}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="8,10"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Color -->
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="COLOR" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Height="40"
|
||||
MaxWidth="194">
|
||||
<ColorPicker
|
||||
Color="{Binding SelectedColor,Converter={StaticResource HexToColorConverter},ConverterParameter=color}" Width="194" Height="40"
|
||||
CornerRadius="{DynamicResource RadiusControl}" IsAlphaEnabled="False" IsAlphaVisible="False" IsColorPaletteVisible="False"
|
||||
IsAccentColorsVisible="False">
|
||||
</ColorPicker>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Actions ──────────────────────── -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Command="{Binding SaveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="{Binding SaveButtonLabel}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
13
Clario/Views/AccountFormView.axaml.cs
Normal file
13
Clario/Views/AccountFormView.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class AccountFormView : UserControl
|
||||
{
|
||||
public AccountFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:views="clr-namespace:Clario.Views"
|
||||
xmlns:model="clr-namespace:Clario.Models"
|
||||
x:DataType="vm:AccountsViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
|
||||
@@ -18,7 +19,7 @@
|
||||
<TextBlock Text="4 accounts" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="Accounts" FontSize="26" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}" Margin="0,2,0,0" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accented" Padding="16,9" VerticalAlignment="Center">
|
||||
<Button Grid.Column="1" Classes="accented" Padding="16,9" VerticalAlignment="Center" Command="{Binding CreateAccountCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/plus.svg" Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
@@ -81,9 +82,11 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Spacing="4">
|
||||
|
||||
<TextBlock Text="{Binding CurrentBalance,StringFormat='$0.00'}" FontSize="15" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" HorizontalAlignment="Right" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" HorizontalAlignment="Right">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" HorizontalAlignment="Right"
|
||||
IsVisible="{Binding !isCredit}">
|
||||
<Svg Width="12" Height="12">
|
||||
<Svg.Path>
|
||||
<MultiBinding Converter="{StaticResource DecimalColorConverter}">
|
||||
@@ -123,6 +126,18 @@
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="5" IsVisible="{Binding isCredit}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Credit Utilisation" FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding CreditUtilizationPerc, StringFormat='0%'}" FontSize="11"
|
||||
Foreground="{DynamicResource AccentYellow}"
|
||||
Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
<ProgressBar Classes="yellow"
|
||||
Value="{Binding CurrentBalance, Converter={StaticResource CreditAmountConverter}}"
|
||||
Minimum="0" Maximum="{Binding CreditLimit}" Width="160" Height="4" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
@@ -155,7 +170,9 @@
|
||||
<TextBlock Text="{Binding SelectedAccount.Name}" FontSize="14" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="{Binding SelectedAccount.Institution}" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2" Background="Transparent" BorderThickness="0" Padding="6" VerticalAlignment="Top" Cursor="Hand">
|
||||
<Button Grid.Column="2" Background="Transparent" BorderThickness="0" Padding="6" VerticalAlignment="Top" Cursor="Hand"
|
||||
Command="{Binding EditAccountCommand
|
||||
}" CommandParameter="{Binding SelectedAccount}">
|
||||
<Svg Path="../Assets/Icons/pencil.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -354,7 +371,9 @@
|
||||
</Button>
|
||||
<!-- Delete -->
|
||||
<Button Background="#2A0D0D" BorderBrush="#3A1515" BorderThickness="1" CornerRadius="10" Padding="14,10"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Cursor="Hand">
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Cursor="Hand"
|
||||
Command="{Binding RequestDeleteAccountCommand}" CommandParameter="{Binding SelectedAccount}"
|
||||
IsEnabled="{Binding CanDeleteAccount}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
@@ -369,5 +388,9 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
<Grid Grid.Row="0" Grid.RowSpan="2">
|
||||
<views:DeleteAccountDialogView IsVisible="{Binding DataContext.IsDeleteDialogVisible ,ElementName=AccountsPage }"
|
||||
DataContext="{Binding Path=DeleteDialog}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- <Border Background="{DynamicResource AccentBlue}" VerticalAlignment="Top" HorizontalAlignment="Left" Height="400" Width="400" Padding="10"> -->
|
||||
<!-- -->
|
||||
<!-- </Border> -->
|
||||
|
||||
|
||||
<!-- Center card -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -38,7 +38,7 @@
|
||||
CornerRadius="16"
|
||||
Height="80"
|
||||
HorizontalAlignment="Center" Margin="0 0 0 10">
|
||||
<Image Source="../Assets/logo-textmark.png"/>
|
||||
<Image Source="../Assets/logo-textmark.png" />
|
||||
</Border>
|
||||
<!-- REPLACE: app name -->
|
||||
<StackPanel Spacing="4" HorizontalAlignment="Center">
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:Clario.Views"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Clario.Views.BudgetCardMenuView">
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="6"
|
||||
Width="196"
|
||||
BoxShadow="0 8 32 0 #3C000000">
|
||||
<StackPanel Spacing="1">
|
||||
|
||||
<!-- Edit -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Padding="10,9"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Cursor="Hand">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="LeftEdgeAlignedTop"
|
||||
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
|
||||
<views:BudgetFormView />
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/pencil.svg" Width="14" Height="14" Css="{DynamicResource SvgSecondary}" />
|
||||
<TextBlock Text="Edit Budget" FontSize="13" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- View Transactions -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Padding="10,9"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/list.svg" Width="14" Height="14" Css="{DynamicResource SvgSecondary}" />
|
||||
<TextBlock Text="View Transactions" FontSize="13" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Reset Spent -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Padding="10,9"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/rotate-ccw.svg" Width="14" Height="14" Css="{DynamicResource SvgSecondary}" />
|
||||
<TextBlock Text="Reset Spent" FontSize="13" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Separator Margin="4,2" />
|
||||
|
||||
<!-- Delete -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Padding="10,9"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="Delete Budget" FontSize="13" Foreground="#FF5E5E" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class BudgetCardMenuView : UserControl
|
||||
{
|
||||
public BudgetCardMenuView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -2,226 +2,412 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:styles="clr-namespace:Clario.Theme.Styles"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Clario.Views.BudgetFormView">
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
Padding="24"
|
||||
Width="380"
|
||||
BoxShadow="0 12 40 0 #4C000000">
|
||||
<StackPanel Spacing="0">
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:behaviors="clr-namespace:Clario.Behaviors"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Clario.Views.BudgetFormView"
|
||||
x:DataType="vm:BudgetFormViewModel">
|
||||
<Design.DataContext>
|
||||
<vm:BudgetFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<!-- ── Header ─────────────────────────── -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,22">
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgBlue}"
|
||||
CornerRadius="10"
|
||||
Width="38" Height="38"
|
||||
Margin="0,0,12,0">
|
||||
<Svg Path="../Assets/Icons/wallet.svg" Width="17" Height="17" Css="{DynamicResource SvgBlue}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
|
||||
<!-- REPLACE: bind to DialogTitle -->
|
||||
<TextBlock Text="Edit Budget"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<!-- REPLACE: bind to SelectedCategory -->
|
||||
<TextBlock Text="Food & Dining"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Padding="6" VerticalAlignment="Top" Cursor="Hand">
|
||||
<Svg Path="../Assets/Icons/x.svg" Width="15" Height="15" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<!-- ── Dim overlay ───────────────────────── -->
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- ── Category ───────────────────────── -->
|
||||
<TextBlock Text="CATEGORY" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Margin="0,0,0,18">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<!-- ── Modal card ────────────────────────── -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="460"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Header ──────────────────────── -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgGreen}"
|
||||
CornerRadius="8"
|
||||
Width="34" Height="34"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<!-- REPLACE: icon changes with SelectedCategory -->
|
||||
<Svg Path="../Assets/Icons/utensils.svg" Width="15" Height="15" Css="{DynamicResource SvgGreen}" />
|
||||
</Border>
|
||||
<!-- REPLACE: SelectedItem="{Binding SelectedCategory}" -->
|
||||
<ComboBox Grid.Column="1"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Padding="10,11" FontSize="13"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectedIndex="0">
|
||||
<ComboBoxItem Content="Food & Dining" />
|
||||
<ComboBoxItem Content="Housing" />
|
||||
<ComboBoxItem Content="Transport" />
|
||||
<ComboBoxItem Content="Health" />
|
||||
<ComboBoxItem Content="Leisure" />
|
||||
<ComboBoxItem Content="Shopping" />
|
||||
<ComboBoxItem Content="Education" />
|
||||
<ComboBoxItem Content="Subscriptions" />
|
||||
<ComboBoxItem Content="Other" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Monthly Limit ──────────────────── -->
|
||||
<TextBlock Text="MONTHLY LIMIT" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="14,0"
|
||||
Margin="0,0,0,8">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Text="$"
|
||||
FontSize="15" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<!-- REPLACE: Text="{Binding LimitAmount, Mode=TwoWay}" -->
|
||||
<TextBox Grid.Column="1" Text="500" Watermark="0.00"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
FontSize="15" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="44" Padding="0" VerticalContentAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Quick-pick amounts -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,18">
|
||||
<Border Background="{DynamicResource BgBase}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="20" Padding="10,4"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="$100" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BgBase}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="20" Padding="10,4"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="$250" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource AccentBlue}" CornerRadius="20" Padding="10,4" Cursor="Hand">
|
||||
<TextBlock Text="$500" FontSize="11" FontWeight="SemiBold" Foreground="{DynamicResource BgBase}" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BgBase}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="20" Padding="10,4"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="$1,000" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Rollover ───────────────────────── -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="14,11"
|
||||
Margin="0,0,0,18">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="Rollover unused budget"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="Carry leftover amount into next month"
|
||||
FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<!-- REPLACE: IsChecked="{Binding RolloverEnabled}" -->
|
||||
<ToggleSwitch Grid.Column="1" styles:ToggleSwitchExtensions.KnobWidth="16"
|
||||
styles:ToggleSwitchExtensions.KnobHeight="16" VerticalAlignment="Center" OffContent="" OnContent="" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Alert Threshold ────────────────── -->
|
||||
<TextBlock Text="ALERT THRESHOLD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Margin="0,0,0,8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="14,11,0,11">
|
||||
<TextBlock Text="Warn me when I reach"
|
||||
FontSize="13" Foreground="{DynamicResource TextSecondary}" />
|
||||
<!-- REPLACE: bind to AlertThresholdLabel -->
|
||||
<TextBlock Text="80% of limit ($400)"
|
||||
FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<!-- Stepper -->
|
||||
<Border Grid.Column="1"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Margin="8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="8,6" Cursor="Hand">
|
||||
<Svg Path="../Assets/Icons/minus.svg" Width="12" Height="12" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
<!-- REPLACE: bind to AlertThreshold -->
|
||||
<TextBlock Text="80%"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" Margin="4,0" />
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="8,6" Cursor="Hand">
|
||||
<Svg Path="../Assets/Icons/plus.svg" Width="12" Height="12" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
CornerRadius="10"
|
||||
Width="42" Height="42"
|
||||
Margin="0,0,14,0">
|
||||
<Svg Path="../Assets/Icons/wallet-cards.svg"
|
||||
Width="18" Height="18"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding FormTitle}"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="{Binding FormSubtitle}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
VerticalAlignment="Top"
|
||||
Cursor="Hand"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Threshold quick-picks -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,24">
|
||||
<Border Background="{DynamicResource BgBase}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="20" Padding="10,4"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="60%" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<!-- ── Category ───────────────────── -->
|
||||
<TextBlock Text="CATEGORY" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="30" Height="30"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedCategory.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedCategory.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="14" Height="14"
|
||||
Css="{Binding SelectedCategory.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding Categories}"
|
||||
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="8,10"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BgBase}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="20" Padding="10,4"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="70%" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource AccentBlue}" CornerRadius="20" Padding="10,4" Cursor="Hand">
|
||||
<TextBlock Text="80%" FontSize="11" FontWeight="SemiBold" Foreground="{DynamicResource BgBase}" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BgBase}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="20" Padding="10,4"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="90%" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Actions ────────────────────────── -->
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<!-- REPLACE: Command="{Binding CancelCommand}" -->
|
||||
<Button Grid.Column="0"
|
||||
Classes="base"
|
||||
Margin="0,0,6,0" Padding="0,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13" Content="Cancel" />
|
||||
<!-- ── Limit Amount ────────────────── -->
|
||||
<TextBlock Text="LIMIT AMOUNT" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,0"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding LimitAmount, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="54"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- REPLACE: Command="{Binding SaveCommand}" -->
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
Margin="6,0,0,0" Padding="0,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center">
|
||||
<!-- ── Period ─────────────────────── -->
|
||||
<TextBlock Text="PERIOD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
<!-- Monthly -->
|
||||
<Button Grid.Column="0"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsMonthly}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetPeriodCommand}"
|
||||
CommandParameter="monthly">
|
||||
<TextBlock Text="Monthly"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<!-- Quarterly -->
|
||||
<Button Grid.Column="1"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsQuarterly}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetPeriodCommand}"
|
||||
CommandParameter="quarterly">
|
||||
<TextBlock Text="Quarterly"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<!-- Yearly -->
|
||||
<Button Grid.Column="2"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsYearly}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetPeriodCommand}"
|
||||
CommandParameter="yearly">
|
||||
<TextBlock Text="Yearly"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Alert Threshold ────────────── -->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,6">
|
||||
<TextBlock Grid.Column="0" Text="ALERT THRESHOLD" Classes="label" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding AlertThresholdLabel}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</Grid>
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,20">
|
||||
<StackPanel Spacing="6">
|
||||
<!-- IMPORTANT: Minimum and Maximum must come before Value -->
|
||||
<Slider Minimum="10"
|
||||
Maximum="100"
|
||||
TickFrequency="5"
|
||||
IsSnapToTickEnabled="True"
|
||||
Value="{Binding AlertThreshold}" />
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="10%"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="100%"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Rollover ───────────────────── -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,8">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="8"
|
||||
Width="32" Height="32"
|
||||
Margin="0,0,12,0"
|
||||
Background="{DynamicResource IconBgPurple}">
|
||||
<Svg Path="../Assets/Icons/refresh-cw.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgPurple}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="Rollover"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="Carry unused budget to the next period"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="2"
|
||||
IsChecked="{Binding Rollover, Mode=TwoWay}"
|
||||
OnContent=""
|
||||
OffContent=""
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,8,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg" Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Save" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}" VerticalAlignment="Center" />
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete button (edit mode only) ── -->
|
||||
<Button HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#1A0808"
|
||||
BorderBrush="#3A1515"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0,10"
|
||||
Margin="0,0,0,10"
|
||||
IsVisible="{Binding IsEditMode}"
|
||||
Command="{Binding RequestDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="Delete Budget"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FF5E5E"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- ── Actions ──────────────────────── -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Command="{Binding SaveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="{Binding SaveButtonLabel}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete confirm sub-modal ──────────────── -->
|
||||
<Grid IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="340"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="22" Height="22"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Delete Budget"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- Description -->
|
||||
<TextBlock Text="This action cannot be undone. The budget will be permanently removed."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24" />
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelDeleteCommand}" />
|
||||
<Button Margin="6,0,0,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FFFFFF; }" />
|
||||
<TextBlock Text="Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,4 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:views="clr-namespace:Clario.Views"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||
xmlns:skiaSharpView="clr-namespace:LiveChartsCore.SkiaSharpView;assembly=LiveChartsCore.SkiaSharpView"
|
||||
xmlns:model="clr-namespace:Clario.Models"
|
||||
x:DataType="vm:BudgetViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
|
||||
x:Class="Clario.Views.BudgetView">
|
||||
x:Class="Clario.Views.BudgetView"
|
||||
x:Name="budgetControl">
|
||||
<Design.DataContext>
|
||||
<vm:BudgetViewModel />
|
||||
</Design.DataContext>
|
||||
@@ -52,7 +50,7 @@
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!-- REPLACE: Command="{Binding PreviousPeriodCommand}" -->
|
||||
<Button Background="Transparent"
|
||||
Classes="nav textless"
|
||||
Classes="nav"
|
||||
BorderThickness="0"
|
||||
Padding="10,8"
|
||||
Cursor="Hand"
|
||||
@@ -70,7 +68,7 @@
|
||||
Margin="4,0" />
|
||||
<!-- REPLACE: Command="{Binding NextPeriodCommand}" -->
|
||||
<Button Background="Transparent"
|
||||
Classes="nav textless"
|
||||
Classes="nav"
|
||||
BorderThickness="0"
|
||||
Padding="10,8"
|
||||
Cursor="Hand"
|
||||
@@ -85,13 +83,8 @@
|
||||
<!-- Add budget button -->
|
||||
<!-- REPLACE: Command="{Binding AddBudgetCommand}" -->
|
||||
<Button Classes="accented"
|
||||
Padding="16,9">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="LeftEdgeAlignedTop"
|
||||
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
|
||||
<views:BudgetFormView />
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
Padding="16,9"
|
||||
Command="{Binding CreateBudgetCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/plus.svg"
|
||||
Width="14" Height="14"
|
||||
@@ -135,6 +128,7 @@
|
||||
Classes="label"
|
||||
Margin="0,0,0,4" />
|
||||
<Border IsVisible="{Binding !GroupHeader}"
|
||||
Classes="editable"
|
||||
Classes.budget-card="{Binding IsOnTrack}"
|
||||
Classes.budget-card-warning="{Binding IsWarning}"
|
||||
Classes.budget-card-over="{Binding IsOverBudget}"
|
||||
@@ -144,7 +138,7 @@
|
||||
Cursor="Hand">
|
||||
<StackPanel Spacing="14">
|
||||
<!-- Header row -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
Width="40" Height="40"
|
||||
@@ -154,9 +148,18 @@
|
||||
Color="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="18" Height="18"
|
||||
Css="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
<Panel>
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="18" Height="18" Classes="hide"
|
||||
Css="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
<Button Classes="base reveal" CornerRadius="{DynamicResource RadiusSmall}" Width="40"
|
||||
Height="40" Margin="0"
|
||||
CommandParameter="{Binding .}"
|
||||
Command="{Binding DataContext.EditBudgetCommand,ElementName=budgetControl}">
|
||||
<Svg Path="../Assets/Icons/pencil.svg" Width="16" Height="16"
|
||||
Css="{DynamicResource SvgSecondary}" />
|
||||
</Button>
|
||||
</Panel>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
|
||||
<!-- REPLACE: bind to Budget.CategoryName -->
|
||||
@@ -192,24 +195,6 @@
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
<!-- Edit button -->
|
||||
<!-- REPLACE: Command="{Binding EditBudgetCommand}" CommandParameter="{Binding}" -->
|
||||
<Button Grid.Column="3"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center"
|
||||
Cursor="Hand">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="BottomEdgeAlignedRight"
|
||||
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
|
||||
<views:BudgetCardMenuView />
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
<Svg Path="../Assets/Icons/ellipsis.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Progress bar + remaining -->
|
||||
@@ -569,7 +554,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
<!-- REPLACE: bind to SavingsGoalFormatted -->
|
||||
<TextBlock Grid.Column="1" Text="{Binding Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold"
|
||||
<TextBlock Grid.Column="1" Text="{Binding AppData.Profile.SavingsGoal, StringFormat='$0'}" FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
@@ -581,7 +567,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- REPLACE: Value="{Binding SavingsGoalPercentage}" -->
|
||||
<ProgressBar Classes="yellow" Value="{Binding TotalLeft}" Minimum="0" Maximum="{Binding Profile.SavingsGoal}" Height="6" />
|
||||
<ProgressBar Classes="yellow" Value="{Binding TotalLeft}" Minimum="0" Maximum="{Binding AppData.Profile.SavingsGoal}" Height="6" />
|
||||
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
CornerRadius="10"
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<!-- Top Bar -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Classes="muted" Text="Friday, March 6, 2026" />
|
||||
<!-- <TextBlock Classes="muted" Text="Friday, March 6, 2026" /> -->
|
||||
<TextBlock Classes="muted" Text="{Binding DateToday}" />
|
||||
<TextBlock Text="Financial Overview" FontSize="{StaticResource FontSizePageTitle}" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" Margin="0,2,0,0" />
|
||||
</StackPanel>
|
||||
@@ -113,7 +114,7 @@
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Spending by Category" FontSize="{StaticResource FontSizeSectionHeading}" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted" Text="March 2026" />
|
||||
<TextBlock Classes="muted" Text="{Binding SelectedChartTimPeriodSubTitle}" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" SelectedIndex="0" ItemsSource="{Binding ChartTimePeriods}"
|
||||
SelectedItem="{Binding SelectedChartTimePeriod}" Background="{DynamicResource BgHover}"
|
||||
@@ -124,7 +125,8 @@
|
||||
<Panel>
|
||||
<StackPanel Spacing="20" IsVisible="{Binding HasSpendingData}">
|
||||
<lvc:CartesianChart Series="{Binding SpendingByCategoryChartSeries}" Height="250" Background="{DynamicResource BgSurface}"
|
||||
LegendPosition="Hidden" TooltipPosition="Hidden" ZoomMode="None" Name="chart">
|
||||
LegendPosition="Hidden" TooltipPosition="Hidden" ZoomMode="None" Name="chart"
|
||||
AnimationsSpeed="00:00:00.1">
|
||||
<lvc:CartesianChart.XAxes>
|
||||
<lvc:XamlAxis IsVisible="False" />
|
||||
</lvc:CartesianChart.XAxes>
|
||||
|
||||
495
Clario/Views/DeleteAccountDialogView.axaml
Normal file
495
Clario/Views/DeleteAccountDialogView.axaml
Normal file
@@ -0,0 +1,495 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Clario.Views.DeleteAccountDialogView"
|
||||
x:DataType="vm:DeleteAccountDialogViewModel">
|
||||
<Design.DataContext>
|
||||
<vm:DeleteAccountDialogViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<!-- Dim overlay -->
|
||||
<Border Background="#70000000"/>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
STEP 1 — Simple confirm (no transactions)
|
||||
═══════════════════════════════════════ -->
|
||||
<Border IsVisible="{Binding IsSimpleConfirmStep}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="380"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="22" Height="22"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }"/>
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Delete Account"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<!-- Account name badge -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border CornerRadius="7"
|
||||
Width="26" Height="26"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15"/>
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Account.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Account.Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Description -->
|
||||
<TextBlock Text="This account has no transactions. It will be permanently deleted and cannot be recovered."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24"/>
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}"/>
|
||||
<Button Margin="6,0,0,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FFFFFF; }"/>
|
||||
<TextBlock Text="Delete Account"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
STEP 1B — Has transactions warning
|
||||
═══════════════════════════════════════ -->
|
||||
<Border IsVisible="{Binding IsHasTransactionsStep}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="400"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/triangle-alert.svg"
|
||||
Width="22" Height="22"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #F5C842; }"/>
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Account Has Transactions"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<!-- Account name badge -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border CornerRadius="7"
|
||||
Width="26" Height="26"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15"/>
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Account.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Account.Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
CornerRadius="6"
|
||||
Padding="6,2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Text="{Binding Account.TransactionsCount}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentYellow}"/>
|
||||
<TextBlock Text="transactions"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AccentYellow}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Description -->
|
||||
<TextBlock FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24">
|
||||
<Run Text="This account has linked transactions. Before deleting, you must migrate them to another account. The transactions will be re-linked and balances will be recalculated."/>
|
||||
</TextBlock>
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}"/>
|
||||
<Button Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding GoToMigrateStepCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/arrow-right-left.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }"/>
|
||||
<TextBlock Text="Migrate & Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
STEP 2 — Pick target account + confirm
|
||||
═══════════════════════════════════════ -->
|
||||
<Border IsVisible="{Binding IsMigrateStep}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="420"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,22">
|
||||
<Button Grid.Column="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center"
|
||||
Cursor="Hand"
|
||||
Command="{Binding BackToWarningCommand}">
|
||||
<Svg Path="../Assets/Icons/arrow-left.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgMuted}"/>
|
||||
</Button>
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="Migrate Transactions"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Choose where to move the transactions"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center"
|
||||
Cursor="Hand"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- From → To visual -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,12"
|
||||
Margin="0,0,0,18">
|
||||
<Grid ColumnDefinitions="*,Auto,*">
|
||||
|
||||
<!-- From account -->
|
||||
<StackPanel Grid.Column="0" Spacing="4" HorizontalAlignment="Center">
|
||||
<TextBlock Text="FROM" Classes="label" HorizontalAlignment="Center"/>
|
||||
<Border CornerRadius="8"
|
||||
Width="38" Height="38"
|
||||
HorizontalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15"/>
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Account.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Account.Name}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"/>
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
CornerRadius="6"
|
||||
Padding="6,2"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock FontSize="10"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
HorizontalAlignment="Center">
|
||||
<Run Text="{Binding Account.TransactionsCount}"/>
|
||||
<Run Text=" transactions"/>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Arrow -->
|
||||
<Svg Grid.Column="1"
|
||||
Path="../Assets/Icons/arrow-right.svg"
|
||||
Width="18" Height="18"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8,0"/>
|
||||
|
||||
<!-- To account -->
|
||||
<StackPanel Grid.Column="2" Spacing="4" HorizontalAlignment="Center">
|
||||
<TextBlock Text="TO" Classes="label" HorizontalAlignment="Center"/>
|
||||
<Border CornerRadius="8"
|
||||
Width="38" Height="38"
|
||||
HorizontalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15"/>
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding TargetAccount.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding TargetAccount.Name}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"/>
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
CornerRadius="6"
|
||||
Padding="6,2"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text="Target"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Target account selector -->
|
||||
<TextBlock Text="SELECT TARGET ACCOUNT" Classes="label" Margin="0,0,0,6"/>
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,18">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="30" Height="30"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15"/>
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding TargetAccount.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="14" Height="14"
|
||||
Css="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding AvailableAccounts}"
|
||||
SelectedItem="{Binding TargetAccount, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="8,10"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Warning info -->
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="8">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/info.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #F5C842; }"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,1,0,0"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentYellow}"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="All transactions will be moved to"/>
|
||||
<Run Text=" "/>
|
||||
<Run Text="{Binding TargetAccount.Name}" FontWeight="SemiBold"/>
|
||||
<Run Text=". Balances will be recalculated. This cannot be undone."/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }"/>
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}"/>
|
||||
<Button Margin="6,0,0,0"
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
IsEnabled="{Binding CanMigrateAndDelete}"
|
||||
Command="{Binding MigrateAndDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FFFFFF; }"/>
|
||||
<TextBlock Text="Migrate & Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
13
Clario/Views/DeleteAccountDialogView.axaml.cs
Normal file
13
Clario/Views/DeleteAccountDialogView.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class DeleteAccountDialogView : UserControl
|
||||
{
|
||||
public DeleteAccountDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -103,12 +103,23 @@
|
||||
Padding="12,10">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Grid Grid.Column="0" ColumnDefinitions="Auto,*" ColumnSpacing="10">
|
||||
<Border Grid.Column="0" Background="{DynamicResource BorderAccent}" CornerRadius="{StaticResource RadiusPill}" Width="34"
|
||||
Height="34">
|
||||
<TextBlock Text="N" FontSize="{StaticResource FontSizeAmount}" FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentBlue}" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Panel Grid.Column="0">
|
||||
|
||||
<Border CornerRadius="40"
|
||||
ClipToBounds="True"
|
||||
Width="34"
|
||||
Height="34"
|
||||
IsVisible="{Binding Profile.HasAvatar}">
|
||||
<Image Source="{Binding Profile.Avatar}"
|
||||
Stretch="UniformToFill" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BorderAccent}" CornerRadius="{StaticResource RadiusPill}" Width="34"
|
||||
Height="34" IsVisible="{Binding !Profile.HasAvatar}">
|
||||
<TextBlock Text="N" FontSize="{StaticResource FontSizeAmount}" FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentBlue}" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</Panel>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Profile.DisplayName}" TextTrimming="CharacterEllipsis"
|
||||
FontSize="{StaticResource FontSizeBody}"
|
||||
FontWeight="SemiBold"
|
||||
@@ -158,7 +169,7 @@
|
||||
<Button Classes="nav" HorizontalAlignment="Stretch" Classes.active="{Binding isOnBudget}" Command="{Binding GoToBudgetCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Svg Path="../Assets/Icons/wallet.svg" Height="14" Width="14" />
|
||||
<TextBlock Text="Budget" FontSize="{StaticResource FontSizeBody}" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Budgets" FontSize="{StaticResource FontSizeBody}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock Classes="label" Text="REPORTS" Margin="12,20,0,10" />
|
||||
@@ -169,7 +180,7 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock Classes="label" Text="SYSTEM" Margin="12,20,0,10" />
|
||||
<Button Classes="nav" HorizontalAlignment="Stretch">
|
||||
<Button Classes="nav" HorizontalAlignment="Stretch" Classes.active="{Binding isOnSettings}" Command="{Binding GoToSettingsCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Svg Path="../Assets/Icons/settings.svg" Height="14" Width="14" />
|
||||
<TextBlock Text="Settings" FontSize="{StaticResource FontSizeBody}" VerticalAlignment="Center" />
|
||||
@@ -178,13 +189,20 @@
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border Grid.Column="0" Background="#70000000" IsVisible="{Binding IsTransactionFormVisible}" />
|
||||
<Border Grid.Column="0" Grid.ColumnSpan="2" Background="#70000000" IsVisible="{Binding IsDimmed}" />
|
||||
<Grid Grid.Column="1">
|
||||
<ContentControl Content="{Binding CurrentView}" />
|
||||
<views:TransactionFormView
|
||||
DataContext="{Binding TransactionFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsTransactionFormVisible,ElementName=MainControl}">
|
||||
</views:TransactionFormView>
|
||||
IsVisible="{Binding DataContext.IsTransactionFormVisible,ElementName=MainControl}" />
|
||||
<views:AccountFormView
|
||||
DataContext="{Binding AccountFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsAccountFormVisible,ElementName=MainControl}" />
|
||||
<views:BudgetFormView
|
||||
DataContext="{Binding BudgetFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsBudgetFormVisible, ElementName=MainControl}" />
|
||||
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
531
Clario/Views/SettingsView.axaml
Normal file
531
Clario/Views/SettingsView.axaml
Normal file
@@ -0,0 +1,531 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
|
||||
x:Class="Clario.Views.SettingsView"
|
||||
x:DataType="vm:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<vm:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="32,28,32,48"
|
||||
Spacing="0"
|
||||
MaxWidth="720">
|
||||
|
||||
<!-- ── Page header ─────────────────────────── -->
|
||||
<StackPanel Margin="0,0,0,28">
|
||||
<TextBlock Text="Settings"
|
||||
FontSize="26"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="Manage your account and preferences"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Global success / error banner ─────────── -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,14"
|
||||
IsVisible="{Binding HasSuccess}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/circle-check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgGreen}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding SuccessMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,14"
|
||||
IsVisible="{Binding HasError}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding ErrorMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
SECTION: Profile
|
||||
══════════════════════════════════════════════ -->
|
||||
<TextBlock Text="PROFILE"
|
||||
Classes="label"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
Padding="22"
|
||||
Margin="0,0,0,24">
|
||||
<StackPanel Spacing="20">
|
||||
|
||||
<!-- Avatar -->
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<StackPanel Grid.Column="0"
|
||||
Spacing="8"
|
||||
Margin="0,0,20,0">
|
||||
<!-- Avatar circle -->
|
||||
<Panel Width="80" Height="80"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- Image (if has avatar) -->
|
||||
<Border CornerRadius="40"
|
||||
ClipToBounds="True"
|
||||
Width="80" Height="80"
|
||||
IsVisible="{Binding HasAvatar}">
|
||||
<Image Source="{Binding AvatarImage}"
|
||||
Stretch="UniformToFill" />
|
||||
</Border>
|
||||
<!-- Initials fallback -->
|
||||
<Border CornerRadius="40"
|
||||
Width="80" Height="80"
|
||||
Background="{DynamicResource BorderAccent}"
|
||||
IsVisible="{Binding !HasAvatar}">
|
||||
<TextBlock Text="{Binding DisplayName[0]}"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentBlue}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<!-- Upload spinner overlay -->
|
||||
<Border CornerRadius="40"
|
||||
Width="80" Height="80"
|
||||
Background="#80000000"
|
||||
IsVisible="{Binding IsUploadingAvatar}">
|
||||
<TextBlock Text="..."
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Avatar actions -->
|
||||
<StackPanel Spacing="4">
|
||||
<Button Classes="base"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,7"
|
||||
FontSize="12"
|
||||
IsEnabled="{Binding !IsUploadingAvatar}"
|
||||
Command="{Binding UploadAvatarCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/upload.svg"
|
||||
Width="12" Height="12"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Upload" FontSize="12" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,4"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
IsVisible="{Binding HasAvatar}"
|
||||
IsEnabled="{Binding !IsUploadingAvatar}"
|
||||
Command="{Binding RemoveAvatarCommand}"
|
||||
Content="Remove" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Profile fields -->
|
||||
<StackPanel Grid.Column="1" Spacing="16">
|
||||
|
||||
<!-- Display name -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="DISPLAY NAME" Classes="label" />
|
||||
<TextBox Text="{Binding DisplayName, Mode=TwoWay}"
|
||||
Watermark="Your name"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Currency + Theme -->
|
||||
<Grid ColumnDefinitions="*,16,*">
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="CURRENCY" Classes="label" />
|
||||
<ComboBox ItemsSource="{Binding Currencies}"
|
||||
SelectedItem="{Binding SelectedCurrency, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="10,8"
|
||||
FontSize="13" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="THEME" Classes="label" />
|
||||
<ComboBox ItemsSource="{Binding ThemeLabels}"
|
||||
SelectedIndex="{Binding SelectedThemeIndex, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="10,8"
|
||||
FontSize="13" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Language -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="LANGUAGE" Classes="label" />
|
||||
<ComboBox ItemsSource="{Binding LanguageLabels}"
|
||||
SelectedIndex="{Binding SelectedLanguageIndex, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="10,8"
|
||||
FontSize="13" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Save button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="20,9"
|
||||
IsEnabled="{Binding !IsSaving}"
|
||||
Command="{Binding SaveProfileCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<Panel>
|
||||
<TextBlock Text="{Binding IsSaving, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Saving...|Save Changes'}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</Panel>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
SECTION: Account Security
|
||||
══════════════════════════════════════════════ -->
|
||||
<TextBlock Text="ACCOUNT & SECURITY"
|
||||
Classes="label"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
Padding="0"
|
||||
Margin="0,0,0,24">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Email row ───────────────────────────── -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="20,0">
|
||||
<Panel>
|
||||
<!-- Normal email display -->
|
||||
<Grid IsVisible="{Binding !IsChangingEmail}"
|
||||
ColumnDefinitions="*,Auto"
|
||||
MinHeight="58">
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="EMAIL ADDRESS"
|
||||
Classes="label" />
|
||||
<TextBlock Text="{Binding MaskedEmail}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
FontWeight="SemiBold" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding StartChangeEmailCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/pencil.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Change"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Change email form -->
|
||||
<StackPanel IsVisible="{Binding IsChangingEmail}"
|
||||
Spacing="12"
|
||||
Margin="0,16,0,16">
|
||||
<TextBlock Text="CHANGE EMAIL ADDRESS"
|
||||
Classes="label" />
|
||||
|
||||
<!-- Email success / error -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasEmailSuccess}">
|
||||
<TextBlock Text="{Binding EmailSuccessMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasEmailError}">
|
||||
<TextBlock Text="{Binding EmailErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<Grid ColumnDefinitions="*,16,*">
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="NEW EMAIL" Classes="label" />
|
||||
<TextBox Text="{Binding NewEmail, Mode=TwoWay}"
|
||||
Watermark="new@email.com"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="CONFIRM WITH PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding EmailConfirmPassword, Mode=TwoWay}"
|
||||
Watermark="Current password"
|
||||
PasswordChar="•"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
HorizontalAlignment="Right">
|
||||
<Button Classes="base"
|
||||
Padding="16,8"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelChangeEmailCommand}" />
|
||||
<Button Classes="accented"
|
||||
Padding="16,8"
|
||||
IsEnabled="{Binding !IsSaving}"
|
||||
Command="{Binding ConfirmChangeEmailCommand}">
|
||||
<TextBlock Text="Update Email"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Password row ───────────────────────── -->
|
||||
<Border Padding="20,0">
|
||||
<!-- Normal password display -->
|
||||
<Panel>
|
||||
<Grid IsVisible="{Binding !IsChangingPassword}"
|
||||
ColumnDefinitions="*,Auto"
|
||||
MinHeight="58">
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="PASSWORD"
|
||||
Classes="label" />
|
||||
<TextBlock Text="••••••••••••"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
LetterSpacing="2" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding StartChangePasswordCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/pencil.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Change"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Change password form -->
|
||||
<StackPanel IsVisible="{Binding IsChangingPassword}"
|
||||
Spacing="12"
|
||||
Margin="0,16,0,16">
|
||||
<TextBlock Text="CHANGE PASSWORD" Classes="label" />
|
||||
|
||||
<!-- Password success / error -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasPasswordSuccess}">
|
||||
<TextBlock Text="{Binding PasswordSuccessMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasPasswordError}">
|
||||
<TextBlock Text="{Binding PasswordErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="CURRENT PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding CurrentPassword, Mode=TwoWay}"
|
||||
Watermark="Enter current password"
|
||||
PasswordChar="•"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,16,*">
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="NEW PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding NewPassword, Mode=TwoWay}"
|
||||
Watermark="Min. 8 characters"
|
||||
PasswordChar="•"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="CONFIRM NEW PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding ConfirmNewPassword, Mode=TwoWay}"
|
||||
Watermark="Repeat new password"
|
||||
PasswordChar="•"
|
||||
FontSize="13"
|
||||
Height="38"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
HorizontalAlignment="Right">
|
||||
<Button Classes="base"
|
||||
Padding="16,8"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelChangePasswordCommand}" />
|
||||
<Button Classes="accented"
|
||||
Padding="16,8"
|
||||
IsEnabled="{Binding !IsSaving}"
|
||||
Command="{Binding ConfirmChangePasswordCommand}">
|
||||
<TextBlock Text="Update Password"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
SECTION: Danger zone
|
||||
══════════════════════════════════════════════ -->
|
||||
<TextBlock Text="SESSION"
|
||||
Classes="label"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
Padding="20">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="Sign Out"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="You will be returned to the login screen."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="#2A0D0D"
|
||||
BorderBrush="#3A1515"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="16,9"
|
||||
Command="{Binding SignOutCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/log-out.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="Sign Out"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FF5E5E"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
13
Clario/Views/SettingsView.axaml.cs
Normal file
13
Clario/Views/SettingsView.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class SettingsView : UserControl
|
||||
{
|
||||
public SettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,7 @@
|
||||
Classes="ghost"
|
||||
SelectionMode="SingleDate"
|
||||
SelectedDates="{Binding Dates}"
|
||||
SelectedDate="{Binding}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="12,10" />
|
||||
<Button Grid.Column="1"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<!-- Period header ─
|
||||
REPLACE: bind TextBlock texts to SelectedPeriodLabel
|
||||
-->
|
||||
<TextBlock Text="MARCH 2026"
|
||||
<TextBlock Text="{Binding DateRangeLabel}"
|
||||
Classes="label"
|
||||
Margin="0,0,0,4" />
|
||||
<TextBlock Text="Summary"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<!-- Avalonia packages -->
|
||||
<!-- Important: keep version in sync! -->
|
||||
<PackageVersion Include="Avalonia" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Controls.ColorPicker" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.3.0" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.6" />
|
||||
@@ -25,6 +26,7 @@
|
||||
<PackageVersion Include="Xaml.Behaviors.Interactions" Version="11.3.6.6" />
|
||||
<PackageVersion Include="Xaml.Behaviors.Interactivity" Version="11.3.6.6" />
|
||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.116.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user