Add analytics page, auth error handling, and period navigation fix
Some checks failed
Build Linux / build (push) Failing after 24s

Features
Analytics Page: Full-featured analytics dashboard with KPI cards, cash flow trend chart, net worth progression, spending patterns by day-of-week, top spending categories, and income sources breakdown. Includes PDF export via QuestPDF for selected periods. Implemented on both desktop and mobile (simplified).
Auth Error Handling: Map Supabase GotrueException errors to AuthError enum with user-friendly messages for login and signup. Display errors in sign-in and sign-up panels.
Dynamic Transaction/Account Counts: Replace hardcoded "46 transactions" and "4 accounts" text with FilteredTransactionCount and ActiveAccountCount properties bound to actual data.

Fixes
Budget Period Navigation: Fix year-aware date comparison in CanGoToPreviousPeriod and CanGoToNextPeriod. Previously only compared months, preventing navigation before January of current year.

Changes
AnalyticsViewModel: Period selector, KPI calculations, chart data builders (cash flow, net worth, day-of-week, top categories, income sources), PDF export
PdfExportService: QuestPDF report generation with print-optimized styling
AuthViewModel: Error display with GotrueException mapping
BudgetViewModel: Year-aware period navigation
TransactionsViewModel: FilteredTransactionCount property
AccountsViewModel: ActiveAccountCount property
MainViewModel: Analytics navigation and AnalyticsViewModel integration
Views: Analytics button wired, error messages displayed, count bindings updated
This commit is contained in:
2026-04-05 23:08:34 +03:00
parent d8dea1913a
commit 61ff949c19
85 changed files with 6988 additions and 543 deletions

View File

@@ -20,6 +20,7 @@
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />

View File

@@ -13,6 +13,7 @@
<PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
<PackageReference Include="FluentAvalonia.ProgressRing"/>
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>

View File

@@ -22,6 +22,7 @@
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" />

View File

@@ -13,6 +13,7 @@
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />

View File

@@ -0,0 +1,16 @@
<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"
>
<circle cx="18.5" cy="17.5" r="3.5" />
<circle cx="5.5" cy="17.5" r="3.5" />
<circle cx="15" cy="5" r="1" />
<path d="M12 17.5V14l-3-3 4-3 2 3h2" />
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,14 @@
<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"
>
<path d="M12 7v14" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,19 @@
<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"
>
<path d="M8 6v6" />
<path d="M15 6v6" />
<path d="M2 12h19.6" />
<path d="M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3" />
<circle cx="7" cy="18" r="2" />
<path d="M9 18h5" />
<circle cx="16" cy="18" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@@ -0,0 +1,14 @@
<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"
>
<path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z" />
<circle cx="12" cy="13" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@@ -0,0 +1,16 @@
<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"
>
<path d="M10 2v2" />
<path d="M14 2v2" />
<path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1" />
<path d="M6 2v2" />
</svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -0,0 +1,17 @@
<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"
>
<path d="M17.596 12.768a2 2 0 1 0 2.829-2.829l-1.768-1.767a2 2 0 0 0 2.828-2.829l-2.828-2.828a2 2 0 0 0-2.829 2.828l-1.767-1.768a2 2 0 1 0-2.829 2.829z" />
<path d="m2.5 21.5 1.4-1.4" />
<path d="m20.1 3.9 1.4-1.4" />
<path d="M5.343 21.485a2 2 0 1 0 2.829-2.828l1.767 1.768a2 2 0 1 0 2.829-2.829l-6.364-6.364a2 2 0 1 0-2.829 2.829l1.768 1.767a2 2 0 0 0-2.828 2.829z" />
<path d="m9.6 14.4 4.8-4.8" />
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@@ -0,0 +1,20 @@
<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"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M7 3v18" />
<path d="M3 7.5h4" />
<path d="M3 12h18" />
<path d="M3 16.5h4" />
<path d="M17 3v18" />
<path d="M17 7.5h4" />
<path d="M17 16.5h4" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -0,0 +1,16 @@
<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"
>
<path d="M12 7v14" />
<path d="M20 11v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8" />
<path d="M7.5 7a1 1 0 0 1 0-5A4.8 8 0 0 1 12 7a4.8 8 0 0 1 4.5-5 1 1 0 0 1 0 5" />
<rect x="3" y="7" width="18" height="4" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 426 B

View File

@@ -0,0 +1,15 @@
<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"
>
<path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z" />
<path d="M22 10v6" />
<path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5" />
</svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@@ -0,0 +1,13 @@
<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"
>
<path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" />
</svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -0,0 +1,14 @@
<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"
>
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z" />
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
</svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -0,0 +1,15 @@
<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"
>
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1,16 @@
<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"
>
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
<path d="M12 22V12" />
<polyline points="3.29 7 12 12 20.71 7" />
<path d="m7.5 4.27 9 5.15" />
</svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -0,0 +1,17 @@
<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"
>
<path d="m12 14-1 1" />
<path d="m13.75 18.25-1.25 1.42" />
<path d="M17.775 5.654a15.68 15.68 0 0 0-12.121 12.12" />
<path d="M18.8 9.3a1 1 0 0 0 2.1 7.7" />
<path d="M21.964 20.732a1 1 0 0 1-1.232 1.232l-18-5a1 1 0 0 1-.695-1.232A19.68 19.68 0 0 1 15.732 2.037a1 1 0 0 1 1.232.695z" />
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1,13 @@
<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"
>
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,17 @@
<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"
>
<circle cx="6" cy="6" r="3" />
<path d="M8.12 8.12 12 12" />
<path d="M20 4 8.12 15.88" />
<circle cx="6" cy="18" r="3" />
<path d="M14.8 14.8 20 20" />
</svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,13 @@
<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"
>
<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z" />
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,14 @@
<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"
>
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
<path d="M12 18h.01" />
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,17 @@
<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"
>
<path d="M11 2v2" />
<path d="M5 2v2" />
<path d="M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1" />
<path d="M8 15a6 6 0 0 0 12 0v-3" />
<circle cx="20" cy="10" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,18 @@
<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"
>
<path d="M8 3.1V7a4 4 0 0 0 8 0V3.1" />
<path d="m9 15-1-1" />
<path d="m15 15 1-1" />
<path d="M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z" />
<path d="m8 19-2 3" />
<path d="m16 19 2 3" />
</svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -0,0 +1,14 @@
<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"
>
<path d="m17 2-5 5-5-5" />
<rect width="20" height="15" x="2" y="7" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1,16 @@
<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"
>
<path d="M8 22h8" />
<path d="M7 10h10" />
<path d="M12 15v7" />
<path d="M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -0,0 +1,13 @@
<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"
>
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" />
</svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net8.0-android</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
@@ -13,8 +13,8 @@
<ItemGroup>
<PackageReference Include="Avalonia"/>
<PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="Avalonia.Controls.ColorPicker"/>
<PackageReference Include="Avalonia.Svg.Skia"/>
<PackageReference Include="Avalonia.Themes.Fluent"/>
<PackageReference Include="Avalonia.Fonts.Inter"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@@ -23,31 +23,38 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm"/>
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" />
<PackageReference Include="Xaml.Behaviors.Interactions" />
<PackageReference Include="Xaml.Behaviors.Interactivity" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
<PackageReference Include="FluentAvalonia.ProgressRing"/>
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>
<PackageReference Include="Supabase"/>
<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>
<ItemGroup>
<Compile Update="MobileViews\MainAppMobile.axaml.cs">
<DependentUpon>MobileMainView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\AccountFormView.axaml.cs">
<DependentUpon>AccountFormView.axaml</DependentUpon>
</Compile>
<Compile Update="MobileViews\MainAppMobile.axaml.cs">
<DependentUpon>MobileMainView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\AccountFormView.axaml.cs">
<DependentUpon>AccountFormView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\CategoryFormView.axaml.cs">
<DependentUpon>CategoryFormView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\AnalyticsView.axaml.cs">
<DependentUpon>AnalyticsView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="devsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<None Update="devsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -129,14 +129,178 @@ public partial class GeneralDataRepo : ObservableObject
}
}
public async Task InsertTransfer(Guid fromAccountId, Guid toAccountId, decimal amount, DateTime date, string? note)
{
try
{
var userId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!);
var pairId = Guid.NewGuid();
var fromCurrency = Accounts.FirstOrDefault(a => a.Id == fromAccountId)?.Currency ?? "";
var toCurrency = Accounts.FirstOrDefault(a => a.Id == toAccountId)?.Currency ?? "";
var toAmount = ConvertAmount(amount, fromCurrency, toCurrency);
var outTx = new Transaction
{
Id = Guid.NewGuid(),
UserId = userId,
AccountId = fromAccountId,
Type = "transfer_out",
Amount = amount,
Description = "Transfer",
Note = note?.Trim(),
Date = date,
TransferPairId = pairId,
};
var inTx = new Transaction
{
Id = Guid.NewGuid(),
UserId = userId,
AccountId = toAccountId,
Type = "transfer_in",
Amount = toAmount,
Description = "Transfer",
Note = note?.Trim(),
Date = date,
TransferPairId = pairId,
};
var outResult = await SupabaseService.Client.From<Transaction>().Insert(outTx);
var inResult = await SupabaseService.Client.From<Transaction>().Insert(inTx);
if (outResult.Models.Count >= 1)
{
var enriched = LinkTransactionCategories(outResult.Models[0]);
LinkTransactionAccounts(enriched);
Transactions.Add(enriched);
}
if (inResult.Models.Count >= 1)
{
var enriched = LinkTransactionCategories(inResult.Models[0]);
LinkTransactionAccounts(enriched);
Transactions.Add(enriched);
}
// Re-enrich both so AccountDisplayText can reference the counterpart (from/to)
LinkTransactionAccounts();
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task UpdateTransfer(Guid transferPairId, Guid fromAccountId, Guid toAccountId, decimal amount, DateTime date, string? note)
{
try
{
var pair = Transactions.Where(t => t.TransferPairId == transferPairId).ToList();
var fromCurrency = Accounts.FirstOrDefault(a => a.Id == fromAccountId)?.Currency ?? "";
var toCurrency = Accounts.FirstOrDefault(a => a.Id == toAccountId)?.Currency ?? "";
var toAmount = ConvertAmount(amount, fromCurrency, toCurrency);
foreach (var tx in pair)
{
tx.AccountId = tx.Type == "transfer_out" ? fromAccountId : toAccountId;
tx.Amount = tx.Type == "transfer_in" ? toAmount : amount;
tx.Date = date;
tx.Note = note?.Trim();
var result = await SupabaseService.Client.From<Transaction>().Update(tx);
if (result.Model is null) continue;
var index = Transactions.IndexOf(tx);
if (index != -1)
{
var enriched = LinkTransactionCategories(result.Model);
LinkTransactionAccounts(enriched);
Transactions[index] = enriched;
}
}
LinkTransactionAccounts();
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task DeleteTransfer(Guid transferPairId)
{
try
{
await SupabaseService.Client.From<Transaction>()
.Where(x => x.TransferPairId == transferPairId)
.Delete();
var pair = Transactions.Where(t => t.TransferPairId == transferPairId).ToList();
foreach (var tx in pair)
Transactions.Remove(tx);
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task<List<Category>> FetchCategories(bool forceRefresh = false)
{
if (Categories.Count != 0 && !forceRefresh) return Categories.ToList();
var categories = await SupabaseService.Client.From<Category>().Get();
Categories = new ObservableCollection<Category>(categories.Models);
return categories.Models;
}
public async Task<Category?> InsertCategory(Category category)
{
try
{
var result = await SupabaseService.Client.From<Category>()
.Insert(category, new QueryOptions() { Returning = QueryOptions.ReturnType.Representation });
if (result.Model is null) return null;
Categories.Add(result.Model);
return result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
return null;
}
}
public async Task UpdateCategory(Category category)
{
try
{
var result = await SupabaseService.Client.From<Category>().Update(category);
if (result.Model is null) return;
var item = Categories.FirstOrDefault(x => x.Id == result.Model.Id);
if (item is null) return;
var index = Categories.IndexOf(item);
if (index != -1) Categories[index] = result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
}
}
public async Task DeleteCategory(Guid id)
{
try
{
await SupabaseService.Client.From<Category>().Where(x => x.Id == id).Delete();
var item = Categories.FirstOrDefault(x => x.Id == id);
if (item is null) return;
Categories.Remove(item);
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task<List<Account>> FetchAccounts(bool forceRefresh = false)
{
if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
@@ -294,7 +458,7 @@ public partial class GeneralDataRepo : ObservableObject
var balance = accountResult.OpeningBalance +
transactionsResult.Sum(t =>
t.Type == "income" ? t.Amount : -t.Amount);
t.Type is "income" or "transfer_in" ? t.Amount : -t.Amount);
accountResult.CurrentBalance = balance;
await SupabaseService.Client
@@ -399,6 +563,20 @@ public partial class GeneralDataRepo : ObservableObject
WeakReferenceMessenger.Default.Send(new RatesRefreshed());
}
/// Converts <paramref name="amount"/> from <paramref name="fromCurrency"/> to
/// <paramref name="toCurrency"/> using the current live rates.
/// Falls back to <paramref name="amount"/> unchanged when currencies match or rates are missing.
private static decimal ConvertAmount(decimal amount, string fromCurrency, string toCurrency)
{
if (string.IsNullOrEmpty(fromCurrency) || string.IsNullOrEmpty(toCurrency)) return amount;
if (fromCurrency.Equals(toCurrency, StringComparison.OrdinalIgnoreCase)) return amount;
if (!CurrencyService.LiveRates.TryGetValue(fromCurrency, out var fromRate)) return amount;
if (!CurrencyService.LiveRates.TryGetValue(toCurrency, out var toRate) || toRate == 0) return amount;
// fromRate = 1 fromCurrency in primary; toRate = 1 toCurrency in primary
// amount * fromRate / toRate = amount converted to toCurrency
return Math.Round(amount * fromRate / toRate, 6);
}
public void LinkTransactionCategories()
{
foreach (var transaction in Transactions)
@@ -441,6 +619,19 @@ public partial class GeneralDataRepo : ObservableObject
tx.OriginalAmountFormatted = tx.IsMultiCurrency
? $"{CurrencyService.GetSymbol(accountCurrency)}{tx.Amount:N2}"
: string.Empty;
if (tx.IsTransfer && tx.TransferPairId.HasValue)
{
var counterpart = Transactions.FirstOrDefault(t => t.TransferPairId == tx.TransferPairId && t.Id != tx.Id);
var counterpartAccount = counterpart is not null ? Accounts.FirstOrDefault(a => a.Id == counterpart.AccountId) : null;
var fromName = tx.IsTransferOut ? (account?.Name ?? "?") : (counterpartAccount?.Name ?? "?");
var toName = tx.IsTransferOut ? (counterpartAccount?.Name ?? "?") : (account?.Name ?? "?");
tx.AccountDisplayText = $"{fromName} → {toName}";
}
else
{
tx.AccountDisplayText = account?.Name ?? "";
}
}
public async Task UpdateSavingsGoal(decimal? goal)

14
Clario/Enums/AuthError.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace Clario.Enums;
public enum AuthError
{
InvalidCredentials,
EmailAlreadyExists,
EmailNotConfirmed,
WeakPassword,
InvalidEmail,
SignupDisabled,
RateLimited,
SessionExpired,
Unknown
}

View File

@@ -0,0 +1,408 @@
<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" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.AccountFormViewMobile"
x:DataType="vm:AccountFormViewModel"
x:Name="AccountFormRoot"
Classes="mobile">
<Design.DataContext>
<vm:AccountFormViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*,Auto"
Background="{DynamicResource BgBase}">
<!-- ── Top bar ──────────────────────────── -->
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
Margin="16,16,16,0">
<!-- Close -->
<Button Grid.Column="0"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="20"
Width="36" Height="36"
Padding="0"
HorizontalContentAlignment="Center"
Command="{Binding CancelCommand}">
<Svg Path="../Assets/Icons/x.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
<!-- Title -->
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding FormTitle}"
FontSize="15"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding FormSubtitle}"
FontSize="11"
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Center" />
</StackPanel>
<!-- Placeholder to keep title centered -->
<Border Grid.Column="2" Width="36" />
</Grid>
<!-- ── Scrollable form ───────────────────── -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Margin="0,16,0,0">
<StackPanel Margin="16,0,16,16" Spacing="0">
<!-- Icon preview + Name -->
<Grid ColumnDefinitions="Auto,*" Margin="0,0,0,20">
<Border Grid.Column="0"
CornerRadius="14"
Width="52" Height="52"
Margin="0,0,14,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="22" Height="22"
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<TextBox Grid.Column="1"
Text="{Binding Name, Mode=TwoWay}"
Watermark="Account name"
FontSize="20"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
Background="Transparent"
BorderThickness="0,0,0,1"
BorderBrush="{DynamicResource BorderSubtle}"
Padding="0,0,0,8"
VerticalContentAlignment="Bottom"
CornerRadius="0" />
</Grid>
<!-- 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,20">
<ComboBox ItemsSource="{Binding AccountTypes}"
SelectedItem="{Binding SelectedType, Mode=TwoWay}"
Background="Transparent"
BorderThickness="0"
Padding="14,12"
FontSize="15"
HorizontalAlignment="Stretch" />
</Border>
<!-- Institution -->
<TextBlock Text="INSTITUTION (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Institution, Mode=TwoWay}"
Watermark="e.g. Chase"
FontSize="15"
Height="48"
Padding="14,0"
VerticalContentAlignment="Center"
Margin="0,0,0,20" />
<!-- Last 4 digits -->
<TextBlock Text="LAST 4 DIGITS (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Mask, Mode=TwoWay}"
Watermark="e.g. 1234"
FontSize="15"
Height="48"
Padding="14,0"
MaxLength="4"
VerticalContentAlignment="Center"
Margin="0,0,0,20">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
<!-- Opening Balance -->
<TextBlock Text="OPENING BALANCE" 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"
Height="52">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0"
Text="$"
FontSize="20"
FontWeight="Bold"
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="20"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
Height="48"
Padding="0"
VerticalContentAlignment="Center">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
</Grid>
</Border>
<!-- Currency -->
<TextBlock Text="CURRENCY" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding CurrencySearch, Mode=TwoWay}"
Watermark="Search currency..."
FontSize="14"
Height="42"
Padding="14,0"
VerticalContentAlignment="Center"
Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="8"
Margin="0,0,0,20">
<ScrollViewer MaxHeight="100"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding FilteredCurrencies}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Classes="nav"
Content="{Binding}"
Margin="2"
Padding="8,6"
CornerRadius="6"
FontSize="13"
Command="{Binding DataContext.SetCurrencyCommand, ElementName=AccountFormRoot}"
CommandParameter="{Binding}">
<Classes.accented>
<MultiBinding Converter="{StaticResource EqualValueConverter}">
<Binding Path="." />
<Binding Path="DataContext.Currency" ElementName="AccountFormRoot" />
</MultiBinding>
</Classes.accented>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<!-- Credit Limit -->
<StackPanel Spacing="6" Margin="0,0,0,20" IsVisible="{Binding IsCredit}">
<TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="14,0"
Height="52">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0"
Text="$"
FontSize="20"
FontWeight="Bold"
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="20"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
Height="48"
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,20">
<cc:DateRangePicker Classes="ghost"
SelectionMode="SingleDate"
SelectedDates="{Binding OpenedAtDates}"
HorizontalAlignment="Stretch"
Padding="14,12" />
</Border>
<!-- Icon -->
<TextBlock Text="ICON" 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="32" Height="32"
Margin="10,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="10,12"
FontSize="15"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
<!-- Color -->
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Height="48"
Margin="0,0,0,20">
<ColorPicker
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
HorizontalAlignment="Stretch"
Height="48"
CornerRadius="{DynamicResource RadiusControl}"
IsAlphaEnabled="False"
IsAlphaVisible="False"
IsColorPaletteVisible="False"
IsAccentColorsVisible="False" />
</Border>
<!-- Primary account toggle -->
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="14,12"
Margin="0,0,0,20">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="Primary Account"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Text="Sets this account's currency as the reference currency"
FontSize="11"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding IsPrimary, Mode=TwoWay}"
OffContent=""
OnContent=""
VerticalAlignment="Center" />
</Grid>
</Border>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,0,0,8"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="13" Height="13"
Css="{DynamicResource SvgRed}" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ── Bottom action bar ─────────────────── -->
<Border Grid.Row="2"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0"
Padding="16,12,16,20">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0"
Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Grid.Column="1"
Classes="accented"
Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding IsValid}"
Command="{Binding SaveCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/check.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="{Binding SaveButtonLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class AccountFormViewMobile : UserControl
{
public AccountFormViewMobile()
{
InitializeComponent();
}
}

View File

@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Clario.ViewModels"
xmlns:model="clr-namespace:Clario.Models"
xmlns:mobileViews="clr-namespace:Clario.MobileViews"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Clario.MobileViews.AccountsViewMobile"
x:DataType="vm:AccountsViewModel"
@@ -30,14 +31,24 @@
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
</StackPanel>
<Button Grid.Column="1"
Classes="accented"
Padding="12,8"
VerticalAlignment="Center">
<Svg Path="../Assets/Icons/plus.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
</Button>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Button Classes="base"
Padding="12,8"
IsVisible="{Binding HasArchivedAccounts}"
Command="{Binding ShowArchivedListCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/archive.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
<TextBlock Text="Archived" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Classes="accented"
Padding="12,8"
Command="{Binding CreateAccountCommand}">
<Svg Path="../Assets/Icons/plus.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
</Button>
</StackPanel>
</Grid>
<!-- Account list -->
@@ -70,11 +81,17 @@
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="2"
Text="{Binding TotalBalance, StringFormat='$0.00'}"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource AccentBlue}"
VerticalAlignment="Center" />
VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalBalance" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Border>
@@ -119,10 +136,23 @@
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="3">
<TextBlock Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<Border IsVisible="{Binding IsPrimary}"
CornerRadius="4"
Padding="5,2"
Background="{DynamicResource IconBgGreen}"
VerticalAlignment="Center">
<TextBlock Text="PRIMARY"
FontSize="9"
FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" />
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding Institution}"
FontSize="12"
@@ -137,7 +167,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Center"
Spacing="3">
<TextBlock Text="{Binding CurrentBalance, StringFormat='$0.00'}"
<TextBlock Text="{Binding CurrentBalanceFormatted}"
FontSize="15"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
@@ -159,7 +189,7 @@
</MultiBinding>
</Svg.Css>
</Svg>
<TextBlock Text="{Binding MonthlyIncrease, Converter={StaticResource DecimalSignConverter}}"
<TextBlock Text="{Binding MonthlyIncreaseFormatted}"
FontSize="11">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource DecimalColorConverter}">
@@ -239,20 +269,32 @@
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
<!-- Close button -->
<Button Grid.Column="2"
Background="{DynamicResource BgBase}"
BorderThickness="0"
CornerRadius="20"
Width="34" Height="34"
Padding="0"
HorizontalContentAlignment="Center"
VerticalAlignment="Top"
x:Name="CloseButton">
<Svg Path="../Assets/Icons/x.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
<!-- Edit + Close buttons -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Top">
<Button Background="{DynamicResource BgBase}"
BorderThickness="0"
CornerRadius="20"
Width="34" Height="34"
Padding="0"
HorizontalContentAlignment="Center"
Command="{Binding EditAccountCommand}"
CommandParameter="{Binding SelectedAccount}">
<Svg Path="../Assets/Icons/pencil.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
<Button Background="{DynamicResource BgBase}"
BorderThickness="0"
CornerRadius="20"
Width="34" Height="34"
Padding="0"
HorizontalContentAlignment="Center"
x:Name="CloseButton">
<Svg Path="../Assets/Icons/x.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
</StackPanel>
</Grid>
</Grid>
@@ -269,7 +311,7 @@
Padding="16,14">
<StackPanel Spacing="4">
<TextBlock Text="CURRENT BALANCE" Classes="label" />
<TextBlock Text="{Binding SelectedAccount.CurrentBalance, StringFormat='$0.00'}"
<TextBlock Text="{Binding SelectedAccount.CurrentBalanceFormatted}"
FontSize="28"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
@@ -329,7 +371,7 @@
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
<TextBlock Text="Money In" FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding SelectedAccount.TotalIncomeThisMonth, StringFormat='$0.00'}" FontSize="14"
<TextBlock Text="{Binding SelectedAccount.TotalIncomeFormatted}" FontSize="14"
FontWeight="SemiBold" Foreground="{DynamicResource AccentGreen}" />
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding SelectedAccount.IncomeTransactionsThisMonth}" FontSize="11"
@@ -342,7 +384,7 @@
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
<TextBlock Text="Money Out" FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding SelectedAccount.TotalExpenseThisMonth, StringFormat='$0.00'}" FontSize="14"
<TextBlock Text="{Binding SelectedAccount.TotalExpenseFormatted}" FontSize="14"
FontWeight="SemiBold" Foreground="{DynamicResource AccentRed}" />
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding SelectedAccount.ExpenseTransactionsThisMonth}" FontSize="11"
@@ -446,7 +488,9 @@
<StackPanel Spacing="10">
<TextBlock Text="Manage" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" Margin="0,0,0,2" />
<Button Background="Transparent" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="10"
Padding="14,10" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left">
Padding="14,10" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Command="{Binding RequestArchiveAccountCommand}"
CommandParameter="{Binding SelectedAccount}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Svg Path="../Assets/Icons/archive.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
<StackPanel Spacing="1">
@@ -456,13 +500,18 @@
</StackPanel>
</StackPanel>
</Button>
<Button Background="#2A0D0D" BorderBrush="#3A1515" BorderThickness="1" CornerRadius="10" Padding="14,10"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left">
<Button Classes="danger"
CornerRadius="10"
Padding="14,10"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
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; }" />
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14" Css="{DynamicResource SvgRed}" />
<StackPanel Spacing="1">
<TextBlock Text="Delete Account" FontSize="12" FontWeight="SemiBold" Foreground="#FF5E5E" />
<TextBlock Text="Delete Account" FontSize="12" FontWeight="SemiBold" />
<TextBlock Text="Permanently removes all data" FontSize="11" Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</StackPanel>
@@ -476,5 +525,12 @@
</Border>
</Grid>
<!-- ── Dialog overlays ───────────────────── -->
<mobileViews:DeleteAccountDialogViewMobile IsVisible="{Binding DataContext.IsDeleteDialogVisible, ElementName=AccountsPage}"
DataContext="{Binding DeleteDialog}" />
<mobileViews:ArchiveAccountDialogViewMobile IsVisible="{Binding IsArchiveDialogVisible}" />
<mobileViews:ArchivedAccountsDialogViewMobile IsVisible="{Binding IsArchivedListVisible}" />
</Grid>
</UserControl>

View File

@@ -12,7 +12,7 @@ namespace Clario.MobileViews;
public partial class AccountsViewMobile : UserControl
{
private bool _sheetVisible = false;
private bool _sheetVisible;
private TranslateTransform SheetTranslate =>
(TranslateTransform)BottomSheet.RenderTransform!;
@@ -24,7 +24,7 @@ public partial class AccountsViewMobile : UserControl
DimOverlay.PointerPressed += async (_, _) => await HideSheet();
CloseButton.Click += async (_, _) => await HideSheet();
AddHandler(Button.ClickEvent, async (sender, e) =>
AddHandler(Button.ClickEvent, async (_, e) =>
{
if (e.Source is Button { DataContext: Account }) await ShowSheet();
}, handledEventsToo: false);

View File

@@ -0,0 +1,161 @@
<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:model="clr-namespace:Clario.Models"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
mc:Ignorable="d"
Classes="mobile"
x:DataType="vm:AnalyticsViewModel"
x:Class="Clario.MobileViews.AnalyticsViewMobile">
<Design.DataContext>
<vm:AnalyticsViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<!-- Top bar -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,12,16,12">
<StackPanel Grid.Column="0">
<TextBlock Classes="muted" Text="Insights &amp; Trends" FontSize="12" />
<TextBlock Text="Analytics" FontSize="22" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" Margin="0,2,0,0" />
</StackPanel>
<ComboBox Grid.Column="1" ItemsSource="{Binding PeriodOptions}"
SelectedItem="{Binding SelectedPeriod}"
Background="{DynamicResource BgSurface}"
Foreground="{DynamicResource TextSecondary}"
BorderBrush="{DynamicResource BorderSubtle}"
CornerRadius="{DynamicResource RadiusControl}"
Padding="8,5" FontSize="12"
VerticalAlignment="Center" />
</Grid>
<ScrollViewer Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="14" Margin="16,0,16,24">
<!-- KPI Cards (2x2 grid) -->
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">
<!-- Total Income -->
<Border Grid.Row="0" Grid.Column="0" Classes="card" Margin="0,0,7,7">
<StackPanel Spacing="8">
<TextBlock Classes="label" Text="INCOME" />
<TextBlock Text="{Binding TotalIncomeFormatted}" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" />
</StackPanel>
</Border>
<!-- Total Expenses -->
<Border Grid.Row="0" Grid.Column="1" Classes="card" Margin="7,0,0,7">
<StackPanel Spacing="8">
<TextBlock Classes="label" Text="EXPENSES" />
<TextBlock Text="{Binding TotalExpensesFormatted}" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource AccentRed}" />
</StackPanel>
</Border>
<!-- Net Savings -->
<Border Grid.Row="1" Grid.Column="0" Classes="card" Margin="0,0,7,0">
<StackPanel Spacing="8">
<TextBlock Classes="label" Text="NET SAVINGS" />
<TextBlock Text="{Binding NetSavingsFormatted}" FontSize="16" FontWeight="Bold"
Foreground="{Binding NetSavingsPositive, Converter={StaticResource BoolToColorConverter}, ConverterParameter='#2ECC8A|#FF5E5E'}" />
</StackPanel>
</Border>
<!-- Savings Rate -->
<Border Grid.Row="1" Grid.Column="1" Classes="card" Margin="7,0,0,0">
<StackPanel Spacing="8">
<TextBlock Classes="label" Text="SAVINGS RATE" />
<TextBlock Text="{Binding SavingsRateFormatted}" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource AccentPurple}" />
</StackPanel>
</Border>
</Grid>
<!-- Cash Flow Chart -->
<Border Classes="card">
<StackPanel Spacing="12">
<TextBlock Text="Cash Flow Trend" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<lvc:CartesianChart Series="{Binding CashFlowSeries}"
XAxes="{Binding CashFlowXAxes}"
YAxes="{Binding CashFlowYAxes}"
Height="200"
Background="{DynamicResource BgSurface}"
LegendPosition="Bottom"
TooltipPosition="Top"
ZoomMode="None"
AnimationsSpeed="00:00:00.2" />
</StackPanel>
</Border>
<!-- Net Worth -->
<Border Classes="card">
<StackPanel Spacing="12">
<TextBlock Text="Net Worth" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<lvc:CartesianChart Series="{Binding NetWorthSeries}"
XAxes="{Binding NetWorthXAxes}"
YAxes="{Binding NetWorthYAxes}"
Height="180"
Background="{DynamicResource BgSurface}"
LegendPosition="Hidden"
TooltipPosition="Top"
ZoomMode="None"
AnimationsSpeed="00:00:00.2" />
</StackPanel>
</Border>
<!-- Top Categories -->
<Border Classes="card">
<StackPanel Spacing="12">
<TextBlock Text="Top Spending Categories" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Text="No expense data for this period."
Classes="muted" IsVisible="{Binding !HasTopCategories}" FontSize="12" />
<ItemsControl ItemsSource="{Binding TopCategories}"
IsVisible="{Binding HasTopCategories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:CategorySpendRow">
<Border Background="{DynamicResource BgHover}" CornerRadius="10"
Padding="12,10" Margin="0,0,0,1">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" CornerRadius="7" Width="30" Height="30" Margin="0,0,10,0">
<Border.Background>
<SolidColorBrush Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
Width="14" Height="14"
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="4">
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<ProgressBar Value="{Binding Percentage}" Minimum="0" Maximum="100"
Height="3" Classes="blue" CornerRadius="2" />
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding AmountFormatted}"
FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" Margin="8,0,0,0" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class AnalyticsViewMobile : UserControl
{
public AnalyticsViewMobile()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,123 @@
<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.MobileViews.ArchiveAccountDialogViewMobile"
x:DataType="vm:AccountsViewModel"
x:CompileBindings="False"
Classes="mobile">
<Design.DataContext>
<vm:AccountsViewModel />
</Design.DataContext>
<Grid>
<Border Background="#70000000" />
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentOrange}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,40">
<StackPanel Spacing="0">
<!-- Handle bar -->
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,20" />
<!-- Icon -->
<Border Background="{DynamicResource IconBgOrange}"
CornerRadius="14"
Width="54" Height="54"
HorizontalAlignment="Center"
Margin="0,0,0,16">
<Svg Path="../Assets/Icons/archive.svg"
Width="22" Height="22"
Css="{DynamicResource SvgOrange}" />
</Border>
<!-- Title -->
<TextBlock Text="Archive Account"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,12" />
<!-- Account badge -->
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
HorizontalAlignment="Center"
Margin="0,0,0,14">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border CornerRadius="7" Width="26" Height="26" VerticalAlignment="Center">
<Border.Background>
<SolidColorBrush
Color="{Binding AccountToArchive.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding AccountToArchive.Icon, Converter={StaticResource SvgPathFromName}}"
Width="13" Height="13"
Css="{Binding AccountToArchive.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<TextBlock Text="{Binding AccountToArchive.Name}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Description -->
<TextBlock Text="This account will be hidden from your active list and won't appear when adding transactions. You can restore it anytime."
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,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelArchiveCommand}" />
<Button Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="{DynamicResource AccentOrange}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmArchiveCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/archive.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Archive"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class ArchiveAccountDialogViewMobile : UserControl
{
public ArchiveAccountDialogViewMobile()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,138 @@
<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:model="clr-namespace:Clario.Models"
mc:Ignorable="d"
x:Class="Clario.MobileViews.ArchivedAccountsDialogViewMobile"
x:Name="ArchivedListView"
x:DataType="vm:AccountsViewModel"
x:CompileBindings="False"
Classes="mobile">
<Design.DataContext>
<vm:AccountsViewModel />
</Design.DataContext>
<Grid>
<Border Background="#70000000" />
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,32">
<StackPanel Spacing="0">
<!-- Handle bar -->
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,18" />
<!-- Header -->
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
<StackPanel Grid.Column="0" Spacing="2">
<TextBlock Text="Archived Accounts"
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Text="Hidden from active lists and transaction forms"
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
<Button Grid.Column="1"
Background="Transparent"
BorderThickness="0"
Padding="6"
VerticalAlignment="Top"
Command="{Binding CloseArchivedListCommand}">
<Svg Path="../Assets/Icons/x.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
</Grid>
<!-- Accounts list -->
<ScrollViewer MaxHeight="400" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ArchivedAccounts}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Account">
<Border Background="{DynamicResource BgBase}"
CornerRadius="10"
Padding="14,12">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Icon -->
<Border Grid.Column="0"
CornerRadius="10"
Width="42" Height="42"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
Width="18" Height="18"
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<!-- Name / meta -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="3">
<TextBlock Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding Type}"
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="·"
FontSize="12"
Foreground="{DynamicResource TextDisabled}" />
<TextBlock Text="{Binding Currency}"
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</StackPanel>
<!-- Restore button -->
<Button Grid.Column="2"
Background="Transparent"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="8"
Padding="12,8"
Command="{Binding DataContext.UnarchiveAccountCommand, ElementName=ArchivedListView}"
CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/rotate-ccw.svg"
Width="13" Height="13"
Css="{DynamicResource SvgBlue}" />
<TextBlock Text="Restore"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource AccentBlue}" />
</StackPanel>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class ArchivedAccountsDialogViewMobile : UserControl
{
public ArchivedAccountsDialogViewMobile()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,387 @@
<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="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.AuthViewMobile"
x:DataType="vm:AuthViewModel"
Classes="mobile"
Background="{DynamicResource BgBase}">
<Design.DataContext>
<vm:AuthViewModel />
</Design.DataContext>
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="24,48,24,48" Spacing="0">
<!-- ── Logo ──────────────────────────────── -->
<StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36">
<Border CornerRadius="16"
Height="80"
Width="80"
HorizontalAlignment="Center"
Margin="0,0,0,8">
<Image Source="{DynamicResource LogoCombinedPrimaryTransparent2x}" />
</Border>
<TextBlock Text="Your personal finance tracker"
FontSize="13"
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Center" />
</StackPanel>
<!-- ── Tab switcher ───────────────────────── -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="3"
Margin="0,0,0,28">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Classes="nav"
Classes.accented="{Binding isSignin}"
CornerRadius="7"
Padding="0,10"
Command="{Binding SetOperationCommand}"
CommandParameter="login">
<TextBlock Text="Sign In"
FontSize="14"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</Button>
<Button Grid.Column="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Classes="nav"
Classes.accented="{Binding isCreateAccount}"
CornerRadius="7"
Padding="0,10"
Command="{Binding SetOperationCommand}"
CommandParameter="signup">
<TextBlock Text="Create Account"
FontSize="14"
HorizontalAlignment="Center" />
</Button>
</Grid>
</Border>
<!-- ── Sign In panel ──────────────────────── -->
<StackPanel Spacing="0" IsVisible="{Binding isSignin}">
<!-- Email -->
<TextBlock Text="EMAIL" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*">
<Svg Grid.Column="0"
Path="../Assets/Icons/mail.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Classes="ghost"
Watermark="you@example.com"
Text="{Binding Email}"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
</Grid>
</Border>
<!-- Password -->
<TextBlock Text="PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,10">
<Grid ColumnDefinitions="Auto,*,Auto">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Classes="ghost"
Watermark="••••••••"
Text="{Binding Password}"
PasswordChar="●"
RevealPassword="{Binding #showPasswordLogin.IsChecked}"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
<ToggleButton Grid.Column="2"
Name="showPasswordLogin"
Background="Transparent"
BorderThickness="0"
Height="48"
Padding="12,0"
VerticalAlignment="Center">
<ToggleButton.Styles>
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ToggleButton.Styles>
<Panel>
<Svg Path="../Assets/Icons/eye.svg"
Width="16" Height="16"
IsVisible="{Binding #showPasswordLogin.IsChecked}"
Css="{DynamicResource SvgMuted}" />
<Svg Path="../Assets/Icons/eye-closed.svg"
Width="16" Height="16"
IsVisible="{Binding !#showPasswordLogin.IsChecked}"
Css="{DynamicResource SvgMuted}" />
</Panel>
</ToggleButton>
</Grid>
</Border>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="12"
Padding="14,10"
Margin="0,0,0,20"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="13"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- Sign In button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,14"
Margin="0,0,0,24"
Command="{Binding ConfirmLoginCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/log-in.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Sign In"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- ── Create Account panel ───────────────── -->
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
<!-- First / Last name -->
<Grid ColumnDefinitions="*,12,*" Margin="0,0,0,16">
<StackPanel Grid.Column="0" Spacing="6">
<TextBlock Text="FIRST NAME" Classes="label" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}">
<TextBox Watermark="First"
Text="{Binding FirstName}"
Classes="ghost"
FontSize="14"
Height="48"
Padding="12,0"
VerticalContentAlignment="Center" />
</Border>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="6">
<TextBlock Text="LAST NAME" Classes="label" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}">
<TextBox Watermark="Last"
Classes="ghost"
Text="{Binding LastName}"
FontSize="14"
Height="48"
Padding="12,0"
VerticalContentAlignment="Center" />
</Border>
</StackPanel>
</Grid>
<!-- Email -->
<TextBlock Text="EMAIL" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*">
<Svg Grid.Column="0"
Path="../Assets/Icons/mail.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Watermark="you@example.com"
Classes="ghost"
Text="{Binding Email}"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
</Grid>
</Border>
<!-- Password -->
<TextBlock Text="PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Watermark="At least 8 characters"
Classes="ghost"
Text="{Binding Password}"
RevealPassword="{Binding #showPasswordSignup.IsChecked}"
PasswordChar="●"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
<ToggleButton Grid.Column="2"
Name="showPasswordSignup"
Background="Transparent"
BorderThickness="0"
Height="48"
Padding="12,0"
VerticalAlignment="Center">
<ToggleButton.Styles>
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ToggleButton.Styles>
<Panel>
<Svg Path="../Assets/Icons/eye.svg"
Width="16" Height="16"
IsVisible="{Binding #showPasswordSignup.IsChecked}"
Css="{DynamicResource SvgMuted}" />
<Svg Path="../Assets/Icons/eye-closed.svg"
Width="16" Height="16"
IsVisible="{Binding !#showPasswordSignup.IsChecked}"
Css="{DynamicResource SvgMuted}" />
</Panel>
</ToggleButton>
</Grid>
</Border>
<!-- Confirm Password -->
<TextBlock Text="CONFIRM PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Watermark="Repeat your password"
PasswordChar="●"
Text="{Binding ConfirmPassword}"
Classes="ghost"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
</Grid>
</Border>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="12"
Padding="14,10"
Margin="0,0,0,20"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="13"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- Create Account button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,14"
Margin="0,0,0,24"
Command="{Binding ConfirmCreateAccountCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/user-plus.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Create Account"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- ── Footer ──────────────────────────────── -->
<Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely."
FontSize="12"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class AuthViewMobile : UserControl
{
public AuthViewMobile()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,393 @@
<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:behaviors="clr-namespace:Clario.Behaviors"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.BudgetFormViewMobile"
x:DataType="vm:BudgetFormViewModel"
Classes="mobile">
<Design.DataContext>
<vm:BudgetFormViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*,Auto"
Background="{DynamicResource BgBase}">
<!-- ── Top bar ──────────────────────────── -->
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
Margin="16,16,16,0">
<!-- Close -->
<Button Grid.Column="0"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="20"
Width="36" Height="36"
Padding="0"
HorizontalContentAlignment="Center"
Command="{Binding CancelCommand}">
<Svg Path="../Assets/Icons/x.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
<!-- Title -->
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding FormTitle}"
FontSize="15"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding FormSubtitle}"
FontSize="11"
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Center" />
</StackPanel>
<!-- Delete (edit mode only) -->
<Button Grid.Column="2"
Background="{DynamicResource DangerButtonBackground}"
BorderBrush="{DynamicResource DangerButtonBorder}"
BorderThickness="1"
CornerRadius="20"
Width="36" Height="36"
Padding="0"
HorizontalContentAlignment="Center"
IsVisible="{Binding IsEditMode}"
Command="{Binding RequestDeleteCommand}">
<Svg Path="../Assets/Icons/trash-2.svg"
Width="14" Height="14"
Css="{DynamicResource SvgRed}" />
</Button>
<!-- Placeholder to keep title centered -->
<Border Grid.Column="2" Width="36" IsVisible="{Binding !IsEditMode}" />
</Grid>
<!-- ── Scrollable form ───────────────────── -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Margin="0,16,0,0">
<StackPanel Margin="16,0,16,16" Spacing="0">
<!-- Limit Amount (hero input) -->
<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"
Height="64">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0"
Text="$"
FontSize="32"
FontWeight="Bold"
Foreground="{DynamicResource TextMuted}"
VerticalAlignment="Center"
Margin="0,0,8,0"
Padding="0,0,0,2" />
<TextBox Grid.Column="1"
Classes="ghost"
Text="{Binding LimitAmount, Mode=TwoWay}"
Watermark="0.00"
FontSize="32"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
Height="54"
Padding="0,0,0,2"
VerticalContentAlignment="Center">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
</Grid>
</Border>
<!-- 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,*" Height="48">
<Border Grid.Column="0"
CornerRadius="7"
Width="32" Height="32"
Margin="10,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"
VerticalAlignment="Stretch"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
DisplayMemberBinding="{Binding Name}"
Background="Transparent"
BorderThickness="0"
Padding="10,12"
FontSize="15"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
<!-- 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"
Height="50">
<Grid ColumnDefinitions="*,*,*">
<Button Grid.Column="0"
Classes="nav"
Classes.accented="{Binding IsMonthly}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalAlignment="Stretch"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetPeriodCommand}"
CommandParameter="monthly">
<TextBlock Text="Monthly" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</Button>
<Button Grid.Column="1"
Classes="nav"
Classes.accented="{Binding IsQuarterly}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalAlignment="Stretch"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetPeriodCommand}"
CommandParameter="quarterly">
<TextBlock Text="Quarterly" FontSize="14" VerticalAlignment="Center" />
</Button>
<Button Grid.Column="2"
Classes="nav"
Classes.accented="{Binding IsYearly}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalAlignment="Stretch"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetPeriodCommand}"
CommandParameter="yearly">
<TextBlock Text="Yearly" FontSize="14" 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="16,12"
Margin="0,0,0,20">
<StackPanel Spacing="6">
<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,12"
Margin="0,0,0,20">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0"
CornerRadius="8"
Width="34" Height="34"
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="15"
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>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,0,0,8"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="13" Height="13"
Css="{DynamicResource SvgRed}" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ── Bottom action bar ─────────────────── -->
<Border Grid.Row="2"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0"
Padding="16,12,16,20">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0"
Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Grid.Column="1"
Classes="accented"
Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding IsValid}"
Command="{Binding SaveCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/check.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="{Binding SaveButtonLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
<!-- ── Delete confirm sub-modal ─────────── -->
<Grid Grid.Row="0" Grid.RowSpan="3" IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" />
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24,0"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="18"
Padding="24">
<StackPanel Spacing="0">
<Border Background="{DynamicResource IconBgRed}"
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="{DynamicResource SvgRed}" />
</Border>
<TextBlock Text="Delete Budget"
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,8" />
<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" />
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelDeleteCommand}" />
<Button Classes="danger"
Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding ConfirmDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class BudgetFormViewMobile : UserControl
{
public BudgetFormViewMobile()
{
InitializeComponent();
}
}

View File

@@ -8,6 +8,7 @@
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.BudgetViewMobile"
x:DataType="vm:BudgetViewModel"
x:Name="budgetRoot"
Classes="mobile">
<Design.DataContext>
<vm:BudgetViewModel />
@@ -63,7 +64,8 @@
</StackPanel>
</Border>
<Button Classes="accented"
Padding="10,8">
Padding="10,8"
Command="{Binding CreateBudgetCommand}">
<Svg Path="../Assets/Icons/plus.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
@@ -93,10 +95,16 @@
<TextBlock Text="Budgeted"
FontSize="10"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding TotalBudgeted, StringFormat='$0'}"
FontSize="13"
<TextBlock FontSize="13"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalBudgeted" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
@@ -112,10 +120,16 @@
<TextBlock Text="Spent"
FontSize="10"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding TotalSpent, StringFormat='$0'}"
FontSize="13"
<TextBlock FontSize="13"
FontWeight="Bold"
Foreground="{DynamicResource AccentRed}" />
Foreground="{DynamicResource AccentRed}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalSpent" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
@@ -248,14 +262,16 @@
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Right" />
</StackPanel>
<!-- Menu -->
<!-- Edit button -->
<Button Grid.Column="3"
Background="Transparent"
BorderThickness="0"
Padding="4"
VerticalAlignment="Center">
<Svg Path="../Assets/Icons/ellipsis.svg"
Width="15" Height="15"
VerticalAlignment="Center"
Command="{Binding DataContext.EditBudgetCommand, ElementName=budgetRoot}"
CommandParameter="{Binding .}">
<Svg Path="../Assets/Icons/pencil.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
</Button>
</Grid>
@@ -419,7 +435,8 @@
<Button Grid.Column="1"
Background="Transparent"
BorderThickness="0"
Padding="4">
Padding="4"
Command="{Binding EditSavingsGoalCommand}">
<Svg Path="../Assets/Icons/pencil.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
</Button>
</Grid>
@@ -427,8 +444,15 @@
<StackPanel Spacing="6">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<TextBlock Grid.Column="1" Text="{Binding AppData.Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Grid.Column="1" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
<Binding Path="AppData.Profile.SavingsGoal" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Projected savings" FontSize="12" Foreground="{DynamicResource TextMuted}" />

View File

@@ -0,0 +1,320 @@
<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.MobileViews.CategoryFormViewMobile"
x:DataType="vm:CategoryFormViewModel"
x:Name="CategoryFormRoot"
Classes="mobile">
<Design.DataContext>
<vm:CategoryFormViewModel />
</Design.DataContext>
<Grid>
<Border Background="#70000000" />
<!-- Bottom sheet -->
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,32">
<StackPanel Spacing="0">
<!-- Handle bar -->
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,18" />
<!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
<Border Grid.Column="0"
CornerRadius="10"
Width="40" Height="40"
Margin="0,0,12,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="17" Height="17"
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="{Binding FormTitle}"
FontSize="15"
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"
Command="{Binding CancelCommand}">
<Svg Path="../Assets/Icons/x.svg"
Width="15" Height="15"
Css="{DynamicResource SvgMuted}" />
</Button>
</Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto" MaxHeight="480">
<StackPanel Spacing="0">
<!-- Name -->
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Name, Mode=TwoWay}"
Watermark="e.g. Groceries"
FontSize="14"
Height="44"
Padding="14,0"
VerticalContentAlignment="Center"
Margin="0,0,0,16" />
<!-- Type toggle -->
<TextBlock Text="TYPE" 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,16">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0"
Classes="nav"
Classes.accented="{Binding IsExpense}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="7"
Padding="0,10"
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="expense">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-up-right.svg" Width="14" Height="14" />
<TextBlock Text="Expense" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Grid.Column="1"
Classes="nav"
Classes.accented="{Binding IsIncome}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="7"
Padding="0,10"
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="income">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-down-left.svg" Width="14" Height="14" />
<TextBlock Text="Income" FontSize="14" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
<!-- Icon picker -->
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="8"
Margin="0,0,0,16">
<ScrollViewer MaxHeight="160"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding CategoryIcons}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="7" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Classes="nav"
Width="44"
Height="44"
Padding="0"
Margin="2"
CornerRadius="8"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding DataContext.SetIconCommand, ElementName=CategoryFormRoot}"
CommandParameter="{Binding}">
<Classes.accented>
<MultiBinding Converter="{StaticResource EqualValueConverter}">
<Binding Path="." />
<Binding Path="DataContext.SelectedIcon" ElementName="CategoryFormRoot" />
</MultiBinding>
</Classes.accented>
<Svg Path="{Binding Converter={StaticResource SvgPathFromName}}"
Width="16" Height="16" />
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<!-- Color -->
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="8"
Margin="0,0,0,20">
<ColorPicker
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
HorizontalAlignment="Stretch"
Height="44"
CornerRadius="{DynamicResource RadiusControl}"
IsAlphaEnabled="False"
IsAlphaVisible="False"
IsColorPaletteVisible="False"
IsAccentColorsVisible="False" />
</Border>
<!-- Error banner -->
<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="{DynamicResource SvgRed}" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- Delete (edit mode only) -->
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,12"
Margin="0,0,0,10"
IsVisible="{Binding IsEditMode}"
IsEnabled="{Binding CanDelete}"
Command="{Binding RequestDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="{Binding CanDelete, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Delete Category|Min. 4 categories required'}"
FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
<!-- Actions -->
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Classes="accented"
Margin="6,0,0,0"
Padding="0,13"
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="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</ScrollViewer>
</StackPanel>
</Border>
<!-- Delete confirm sub-modal -->
<Grid IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" />
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24,0"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="18"
Padding="24">
<StackPanel Spacing="0">
<Border Background="{DynamicResource IconBgRed}"
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="{DynamicResource SvgRed}" />
</Border>
<TextBlock Text="Delete Category"
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,8" />
<TextBlock Text="This action cannot be undone. The category will be permanently removed."
FontSize="13"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,24" />
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelDeleteCommand}" />
<Button Classes="danger"
Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding ConfirmDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class CategoryFormViewMobile : UserControl
{
public CategoryFormViewMobile()
{
InitializeComponent();
}
}

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<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"
@@ -7,34 +7,42 @@
xmlns:model="clr-namespace:Clario.Models"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:DataType="vm:DashboardViewModel"
x:Class="Clario.MobileViews.DashboardViewMobile">
x:Class="Clario.MobileViews.DashboardViewMobile"
x:Name="DashboardRoot"
Classes="mobile">
<Design.DataContext>
<vm:DashboardViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*"
Background="{DynamicResource BgBase}">
<Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
<!-- ── Top bar ────────────────────────────── -->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="16,16,16,12">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
<StackPanel Grid.Column="0">
<TextBlock Text="Financial Overview"
FontSize="22"
FontWeight="Bold"
FontSize="22" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted"
Text="Friday, March 6, 2026"
FontSize="12"
Margin="0,2,0,0" />
<TextBlock Classes="muted" Text="{Binding DateToday}" FontSize="12" Margin="0,2,0,0" />
</StackPanel>
<!-- Settings button -->
<Button Grid.Column="1"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="10"
Padding="9,8"
Margin="0,0,8,0"
VerticalAlignment="Center"
Command="{Binding NavigateToSettingsCommand}">
<Svg Path="../Assets/Icons/settings.svg" Width="16" Height="16" Css="{DynamicResource SvgMuted}" />
</Button>
<!-- Add transaction button -->
<Button Grid.Column="2"
Classes="accented"
Padding="12,8"
VerticalAlignment="Center">
<Svg Path="../Assets/Icons/plus.svg"
Width="16" Height="16"
VerticalAlignment="Center"
Command="{Binding CreateTransactionCommand}">
<Svg Path="../Assets/Icons/plus.svg" Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
</Button>
</Grid>
@@ -61,22 +69,22 @@
<Border Background="{DynamicResource IconBgGreen}"
CornerRadius="{StaticResource RadiusIcon}"
Padding="5">
<Svg Path="../Assets/Icons/trending-up.svg"
Height="12" Width="12"
<Svg Path="../Assets/Icons/trending-up.svg" Height="12" Width="12"
Css="{DynamicResource SvgGreen}" />
</Border>
<TextBlock Text="INCOME"
Classes="label"
VerticalAlignment="Center" />
<TextBlock Text="INCOME" Classes="label" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding MonthlyIncome, StringFormat='$0.00'}"
FontSize="18"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="18" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="MonthlyIncome" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<Border Classes="badge-green" HorizontalAlignment="Left">
<TextBlock Text="{Binding MonthlyIncomeChangeFormatted}"
FontSize="10"
FontWeight="SemiBold"
FontSize="10" FontWeight="SemiBold"
Foreground="{DynamicResource AccentGreen}" />
</Border>
</StackPanel>
@@ -95,22 +103,22 @@
<Border Background="{DynamicResource IconBgOrange}"
CornerRadius="{StaticResource RadiusIcon}"
Padding="5">
<Svg Path="../Assets/Icons/trending-down.svg"
Height="12" Width="12"
<Svg Path="../Assets/Icons/trending-down.svg" Height="12" Width="12"
Css="{DynamicResource SvgRed}" />
</Border>
<TextBlock Text="EXPENSES"
Classes="label"
VerticalAlignment="Center" />
<TextBlock Text="EXPENSES" Classes="label" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding MonthlyExpenses, StringFormat='$0.00'}"
FontSize="18"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="18" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="MonthlyExpenses" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<Border Classes="badge-red" HorizontalAlignment="Left">
<TextBlock Text="{Binding MonthlyExpenseChangeFormatted}"
FontSize="10"
FontWeight="SemiBold"
FontSize="10" FontWeight="SemiBold"
Foreground="{DynamicResource AccentRed}" />
</Border>
</StackPanel>
@@ -129,13 +137,10 @@
<Border Background="{DynamicResource IconBgPurple}"
CornerRadius="{StaticResource RadiusIcon}"
Padding="5">
<Svg Path="../Assets/Icons/landmark.svg"
Height="12" Width="12"
<Svg Path="../Assets/Icons/landmark.svg" Height="12" Width="12"
Css="{DynamicResource SvgPurple}" />
</Border>
<TextBlock Text="SAVINGS RATE"
Classes="label"
VerticalAlignment="Center" />
<TextBlock Text="SAVINGS RATE" Classes="label" VerticalAlignment="Center" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<ProgressBar Grid.Column="0"
@@ -145,8 +150,7 @@
Value="{Binding MonthlyExpenses}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
FontSize="15"
FontWeight="Bold"
FontSize="15" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
Margin="12,0,0,0">
<TextBlock.Text>
@@ -170,75 +174,100 @@
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="Spending by Category"
FontSize="14"
FontWeight="SemiBold"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="March 2026" FontSize="11" />
<TextBlock Classes="muted" Text="{Binding SelectedChartTimPeriodSubTitle}" FontSize="11" />
</StackPanel>
<ComboBox Grid.Column="1"
SelectedIndex="0"
ItemsSource="{Binding ChartTimePeriods}"
SelectedItem="{Binding SelectedChartTimePeriod}"
Background="{DynamicResource BgHover}"
Foreground="{DynamicResource TextSecondary}"
BorderBrush="{DynamicResource BorderAccent}"
CornerRadius="{StaticResource RadiusIcon}"
Padding="8,5"
FontSize="12" />
Padding="8,5" FontSize="12" />
</Grid>
<lvc:CartesianChart Series="{Binding SpendingByCategoryChartSeries}"
Height="180"
Background="{DynamicResource BgSurface}"
LegendPosition="Hidden"
TooltipPosition="Hidden">
<lvc:CartesianChart.XAxes>
<lvc:XamlAxis IsVisible="False" />
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:XamlLogarithmicAxis LogBase="10" IsVisible="False" MinLimit="1" />
</lvc:CartesianChart.YAxes>
</lvc:CartesianChart>
<Panel>
<!-- Chart + labels (when there is data) -->
<StackPanel Spacing="8" IsVisible="{Binding HasSpendingData}">
<lvc:CartesianChart Series="{Binding SpendingByCategoryChartSeries}"
Height="180"
Background="{DynamicResource BgSurface}"
LegendPosition="Hidden"
TooltipPosition="Hidden"
ZoomMode="None"
AnimationsSpeed="00:00:00.1">
<lvc:CartesianChart.XAxes>
<lvc:XamlAxis IsVisible="False" />
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:XamlLogarithmicAxis LogBase="10" IsVisible="False" MinLimit="1" />
</lvc:CartesianChart.YAxes>
</lvc:CartesianChart>
<!-- Category labels -->
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="model:ColumnChartData">
<TextBlock Text="{Binding Name}"
HorizontalAlignment="Center"
FontSize="10"
Foreground="{DynamicResource TextDisabled}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Category labels -->
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="model:ColumnChartData">
<TextBlock Text="{Binding Name}"
HorizontalAlignment="Center"
FontSize="10"
Foreground="{DynamicResource TextDisabled}"
TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border HorizontalAlignment="Stretch"
Height="1"
Background="{DynamicResource BorderSubtle}" />
<Border HorizontalAlignment="Stretch" Height="1"
Background="{DynamicResource BorderSubtle}" />
<!-- Category amounts -->
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="model:ColumnChartData">
<TextBlock Text="{Binding Values, Converter={StaticResource FirstValueConverter}, StringFormat='$0,00'}"
HorizontalAlignment="Center"
FontSize="10"
FontWeight="SemiBold"
Foreground="{Binding Fill, Converter={StaticResource SkPaintToBrushConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Category amounts -->
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="model:ColumnChartData">
<TextBlock HorizontalAlignment="Center"
FontSize="10" FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
Foreground="{Binding Fill, Converter={StaticResource SkPaintToBrushConverter}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="DataContext.PrimarySymbol" ElementName="DashboardRoot" />
<Binding Path="Values" Converter="{StaticResource FirstValueConverter}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Spacing="10" Margin="0,40"
IsVisible="{Binding !HasSpendingData}">
<Svg Path="../Assets/Icons/chart-column.svg" Css="{DynamicResource SvgDisabled}"
Height="36" Width="36" HorizontalAlignment="Center" />
<TextBlock Text="No spending data"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
<TextBlock Text="Add expense transactions to see your spending breakdown."
FontSize="12" Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" MaxWidth="220" />
</StackPanel>
</Panel>
</StackPanel>
</Border>
@@ -252,81 +281,92 @@
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="Recent Transactions"
FontSize="14"
FontWeight="SemiBold"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Last 5 transactions" FontSize="11" />
</StackPanel>
<Button Grid.Column="1"
Background="Transparent"
Foreground="{DynamicResource AccentBlue}"
BorderThickness="0"
FontSize="12"
Padding="0"
Content="View all →"
VerticalAlignment="Center"
BorderThickness="0" FontSize="12" Padding="0"
Content="View all →" VerticalAlignment="Center"
Command="{Binding ViewAllTransactionsCommand}" />
</Grid>
<!-- Transaction rows -->
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
<ItemsControl ItemsSource="{Binding RecentTransactions}"
Background="{DynamicResource BgSurface}"
CornerRadius="10">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Transaction">
<Grid ColumnDefinitions="Auto,*,Auto"
<Panel>
<!-- Transaction rows -->
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10"
IsVisible="{Binding HasTransactionData}">
<ItemsControl ItemsSource="{Binding RecentTransactions}"
Background="{DynamicResource BgSurface}"
Margin="12,10">
<Border Grid.Column="0"
CornerRadius="9"
Width="36" Height="36"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Height="16" Width="16"
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding Description}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextSecondary}" />
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding Category.Name}" Classes="muted" FontSize="11" />
<TextBlock Text="·" Classes="muted" FontSize="11" />
<TextBlock
Text="{Binding Date, Converter={StaticResource DateFormatConverter}, ConverterParameter='MMM d'}"
Classes="muted" FontSize="11" />
CornerRadius="10">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Transaction">
<Grid ColumnDefinitions="Auto,*,Auto"
Background="{DynamicResource BgSurface}"
Margin="12,10">
<Border Grid.Column="0"
CornerRadius="9" Width="36" Height="36"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Height="16" Width="16"
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding Description}"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextSecondary}" />
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding Category.Name}" Classes="muted" FontSize="11" />
<TextBlock Text="·" Classes="muted" FontSize="11" />
<TextBlock
Text="{Binding Date, Converter={StaticResource DateFormatConverter}, ConverterParameter='MMM d'}"
Classes="muted" FontSize="11" />
</StackPanel>
</StackPanel>
</StackPanel>
<TextBlock Grid.Column="2"
FontSize="13"
FontWeight="SemiBold"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource AmountSignConverter}">
<Binding Path="Amount" />
<Binding Path="Type" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<TextBlock Grid.Column="2"
FontSize="13" FontWeight="SemiBold"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource AmountSignConverter}">
<Binding Path="Amount" />
<Binding Path="Type" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Spacing="10" Margin="0,32"
IsVisible="{Binding !HasTransactionData}">
<Svg Path="../Assets/Icons/receipt.svg" Css="{DynamicResource SvgDisabled}"
Height="36" Width="36" HorizontalAlignment="Center" />
<TextBlock Text="No transactions yet"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
<TextBlock Text="Add your first transaction to get started."
FontSize="12" Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" MaxWidth="220" />
</StackPanel>
</Panel>
</StackPanel>
</Border>
@@ -339,57 +379,75 @@
<StackPanel Spacing="14">
<StackPanel>
<TextBlock Text="Budget Tracker"
FontSize="14"
FontWeight="SemiBold"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Monthly limits" FontSize="11" />
</StackPanel>
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="14" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Budget">
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Height="13" Width="13"
Css="{DynamicResource SvgSecondary}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding Category.Name}"
FontSize="13"
VerticalAlignment="Center"
Foreground="{DynamicResource TextSecondary}" />
</StackPanel>
<Panel Grid.Column="1">
<StackPanel Orientation="Horizontal" IsVisible="{Binding !IsOverBudget}">
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}" FontSize="11"
Foreground="{DynamicResource TextMuted}" />
<Panel>
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}"
IsVisible="{Binding HasBudgetData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="14" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Budget">
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Height="13" Width="13"
Css="{DynamicResource SvgSecondary}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding Category.Name}"
FontSize="13" VerticalAlignment="Center"
Foreground="{DynamicResource TextSecondary}" />
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsOverBudget}">
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="11" Foreground="{DynamicResource AccentRed}" />
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource AccentRed}" />
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}" FontSize="11"
Foreground="{DynamicResource AccentRed}" />
</StackPanel>
</Panel>
</Grid>
<ProgressBar Classes.green="{Binding IsOnTrack}"
Classes.yellow="{Binding IsWarning}"
Classes.red="{Binding IsOverBudget}"
Minimum="0"
Value="{Binding Spent}"
Maximum="{Binding LimitAmount}"
Height="5" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Panel Grid.Column="1">
<StackPanel Orientation="Horizontal" IsVisible="{Binding !IsOverBudget}">
<TextBlock Text="{Binding SpentFormatted}"
FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding LimitFormatted}"
FontSize="11" Foreground="{DynamicResource TextMuted}" />
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsOverBudget}">
<TextBlock Text="{Binding SpentFormatted}"
FontSize="11" Foreground="{DynamicResource AccentRed}" />
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource AccentRed}" />
<TextBlock Text="{Binding LimitFormatted}"
FontSize="11" Foreground="{DynamicResource AccentRed}" />
</StackPanel>
</Panel>
</Grid>
<ProgressBar Classes.green="{Binding IsOnTrack}"
Classes.yellow="{Binding IsWarning}"
Classes.red="{Binding IsOverBudget}"
Minimum="0" Value="{Binding Spent}"
Maximum="{Binding LimitAmount}" Height="5" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Spacing="10" Margin="0,32"
IsVisible="{Binding !HasBudgetData}">
<Svg Path="../Assets/Icons/wallet.svg" Css="{DynamicResource SvgDisabled}"
Height="36" Width="36" HorizontalAlignment="Center" />
<TextBlock Text="No budgets set"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
<TextBlock Text="Create budgets to track your spending limits."
FontSize="12" Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" MaxWidth="200" />
</StackPanel>
</Panel>
</StackPanel>
</Border>
@@ -402,8 +460,7 @@
<StackPanel Spacing="14">
<StackPanel>
<TextBlock Text="Accounts"
FontSize="14"
FontWeight="SemiBold"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="{Binding AccountsSubtitle}" FontSize="11" />
</StackPanel>
@@ -423,8 +480,7 @@
Background="{DynamicResource BgBase}"
Margin="12,10">
<Border Grid.Column="0"
CornerRadius="9"
Width="34" Height="34"
CornerRadius="9" Width="34" Height="34"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
@@ -437,17 +493,14 @@
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}"
FontSize="13"
FontWeight="SemiBold"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextSecondary}" />
<TextBlock Text="{Binding Mask, Converter={StaticResource MaskToStringConverter}}"
FontSize="11"
Classes="muted" />
FontSize="11" Classes="muted" />
</StackPanel>
<TextBlock Grid.Column="2"
Text="{Binding CurrentBalance, StringFormat='$0'}"
FontSize="13"
FontWeight="Bold"
Text="{Binding CurrentBalanceFormatted}"
FontSize="13" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
</Grid>
@@ -459,17 +512,20 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="Total Balance"
FontSize="13"
FontWeight="SemiBold"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextMuted}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
Text="{Binding TotalNetworth, StringFormat=$0}"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
FontSize="17" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalNetworth" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</StackPanel>
</Border>
@@ -477,4 +533,4 @@
</ScrollViewer>
</Grid>
</UserControl>
</UserControl>

View File

@@ -0,0 +1,454 @@
<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.MobileViews.DeleteAccountDialogViewMobile"
x:DataType="vm:DeleteAccountDialogViewModel"
Classes="mobile">
<Design.DataContext>
<vm:DeleteAccountDialogViewModel />
</Design.DataContext>
<Grid>
<Border Background="#70000000" />
<!-- STEP 1 — Simple confirm (no transactions) -->
<Border IsVisible="{Binding IsSimpleConfirmStep}"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,40">
<StackPanel Spacing="0">
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,20" />
<Border Background="{DynamicResource IconBgRed}"
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="{DynamicResource SvgRed}" />
</Border>
<TextBlock Text="Delete Account"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,12" />
<!-- Account badge -->
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
HorizontalAlignment="Center"
Margin="0,0,0,14">
<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>
<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" />
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Classes="danger"
Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding ConfirmDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Delete Account" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
<!-- STEP 1B — Has transactions warning -->
<Border IsVisible="{Binding IsHasTransactionsStep}"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentYellow}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,40">
<StackPanel Spacing="0">
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,20" />
<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="{DynamicResource SvgYellow}" />
</Border>
<TextBlock Text="Account Has Transactions"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,12" />
<!-- Account badge -->
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
HorizontalAlignment="Center"
Margin="0,0,0,14">
<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>
<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." />
</TextBlock>
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Classes="accented"
Margin="6,0,0,0"
Padding="0,13"
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 &amp; Delete"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
<!-- STEP 2 — Pick target account + confirm -->
<Border IsVisible="{Binding IsMigrateStep}"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,40">
<StackPanel Spacing="0">
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,20" />
<!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
<Button Grid.Column="0"
Background="Transparent"
BorderThickness="0"
Padding="4"
VerticalAlignment="Center"
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"
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,*">
<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>
<Svg Grid.Column="1"
Path="../Assets/Icons/arrow-right.svg"
Width="18" Height="18"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="8,0" />
<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,16">
<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,13"
FontSize="14"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
<!-- Warning info -->
<Border Background="{DynamicResource BadgeBgYellow}"
BorderBrush="{DynamicResource AccentYellow}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Svg Grid.Column="0"
Path="../Assets/Icons/info.svg"
Width="13" Height="13"
Css="{DynamicResource SvgYellow}"
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="{DynamicResource SvgRed}" />
<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,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Classes="danger"
Margin="6,0,0,0"
Padding="0,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding CanMigrateAndDelete}"
Command="{Binding MigrateAndDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Migrate &amp; Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class DeleteAccountDialogViewMobile : UserControl
{
public DeleteAccountDialogViewMobile()
{
InitializeComponent();
}
}

View File

@@ -3,7 +3,6 @@
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:mobileViews="clr-namespace:Clario.MobileViews"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Clario.MobileViews.MainViewMobile"
@@ -19,6 +18,18 @@
DataContext="{Binding TransactionFormViewModel}"
IsVisible="{Binding DataContext.IsTransactionFormVisible,ElementName=MainControl}">
</mobileViews:TransactionFormViewMobile>
<mobileViews:AccountFormViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
DataContext="{Binding AccountFormViewModel}"
IsVisible="{Binding DataContext.IsAccountFormVisible, ElementName=MainControl}" />
<mobileViews:BudgetFormViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
DataContext="{Binding BudgetFormViewModel}"
IsVisible="{Binding DataContext.IsBudgetFormVisible, ElementName=MainControl}" />
<mobileViews:CategoryFormViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
DataContext="{Binding CategoryFormViewModel}"
IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" />
<mobileViews:SetSavingsGoalDialogViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
DataContext="{Binding SetSavingsGoalDialogViewModel}"
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
<!-- ── Content area ──────────────────────── -->
<ContentControl Grid.Row="0"
Content="{Binding CurrentView}" />

View File

@@ -0,0 +1,150 @@
<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:behaviors="clr-namespace:Clario.Behaviors"
mc:Ignorable="d"
x:Class="Clario.MobileViews.SetSavingsGoalDialogViewMobile"
x:DataType="vm:SetSavingsGoalDialogViewModel"
Classes="mobile">
<Design.DataContext>
<vm:SetSavingsGoalDialogViewModel />
</Design.DataContext>
<Grid>
<Border Background="#70000000" />
<!-- Bottom sheet -->
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentBlue}"
BorderThickness="0,1,0,0"
CornerRadius="18,18,0,0"
Padding="20,20,20,40">
<StackPanel Spacing="0">
<!-- Handle bar -->
<Border Width="36" Height="4"
CornerRadius="2"
Background="{DynamicResource BorderSubtle}"
HorizontalAlignment="Center"
Margin="0,0,0,20" />
<!-- Icon -->
<Border Background="{DynamicResource IconBgBlue}"
CornerRadius="14"
Width="54" Height="54"
HorizontalAlignment="Center"
Margin="0,0,0,16">
<Svg Path="../Assets/Icons/target.svg"
Width="22" Height="22"
Css="{DynamicResource SvgBlue}" />
</Border>
<!-- Title -->
<TextBlock Text="Set Savings Goal"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,4" />
<!-- Subtitle -->
<TextBlock Text="Set a monthly savings target to track your progress on the budget page."
FontSize="12"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,24" />
<!-- Amount input -->
<TextBlock Text="MONTHLY GOAL" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Margin="0,0,0,6">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0"
Text="$"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextMuted}"
VerticalAlignment="Center"
Margin="16,0,0,0" />
<TextBox Grid.Column="1"
Classes="ghost"
Text="{Binding GoalInput, Mode=TwoWay}"
Watermark="0.00"
FontSize="16"
FontWeight="SemiBold"
Padding="8,14">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
</Grid>
</Border>
<TextBlock Text="Set to 0 to remove the goal"
FontSize="11"
Foreground="{DynamicResource TextDisabled}"
Margin="0,0,0,20" />
<!-- 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="{DynamicResource SvgRed}" />
<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,13"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="14"
Content="Cancel"
Command="{Binding CancelCommand}" />
<Button Classes="accented"
Margin="6,0,0,0"
Padding="0,13"
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="{DynamicResource SvgBase}" />
<TextBlock Text="Save Goal"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class SetSavingsGoalDialogViewMobile : UserControl
{
public SetSavingsGoalDialogViewMobile()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,486 @@
<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="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.SettingsViewMobile"
x:DataType="vm:SettingsViewModel"
Classes="mobile"
Background="{DynamicResource BgBase}">
<Design.DataContext>
<vm:SettingsViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<!-- ── Top bar ────────────────────────────── -->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="16,16,16,12">
<StackPanel Grid.Column="0">
<TextBlock Text="Settings"
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
</StackPanel>
</Grid>
<!-- ── Scrollable content ────────────────── -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,32" Spacing="0">
<!-- ── Global success / error banners ── -->
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
CornerRadius="12"
Padding="14,10"
Margin="0,0,0,12"
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,12"
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="14"
Padding="16"
Margin="0,0,0,20">
<StackPanel Spacing="16">
<!-- Avatar row -->
<Grid ColumnDefinitions="Auto,*">
<Panel Grid.Column="0"
Width="64" Height="64"
Margin="0,0,16,0">
<Border CornerRadius="32"
ClipToBounds="True"
Width="64" Height="64"
IsVisible="{Binding HasAvatar}">
<Image Source="{Binding AvatarImage}" Stretch="UniformToFill" />
</Border>
<Border CornerRadius="32"
Width="64" Height="64"
Background="{DynamicResource BorderAccent}"
IsVisible="{Binding !HasAvatar}">
<TextBlock Text="{Binding DisplayName[0]}"
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource AccentBlue}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<Border CornerRadius="32"
Width="64" Height="64"
Background="#80000000"
IsVisible="{Binding IsUploadingAvatar}">
<TextBlock Text="..."
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Panel>
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="8">
<Button Classes="base"
Padding="14,8"
IsEnabled="{Binding !IsUploadingAvatar}"
Command="{Binding UploadAvatarCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/upload.svg"
Width="13" Height="13"
Css="{DynamicResource SvgMuted}" />
<TextBlock Text="Upload Photo" FontSize="13" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Background="Transparent"
BorderThickness="0"
Padding="0"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
IsVisible="{Binding HasAvatar}"
IsEnabled="{Binding !IsUploadingAvatar}"
Command="{Binding RemoveAvatarCommand}"
Content="Remove photo" />
</StackPanel>
</Grid>
<Separator />
<!-- Display name -->
<StackPanel Spacing="6">
<TextBlock Text="DISPLAY NAME" Classes="label" />
<TextBox Text="{Binding DisplayName, Mode=TwoWay}"
Watermark="Your name"
FontSize="14"
Height="44"
Padding="12,0"
VerticalContentAlignment="Center" />
</StackPanel>
<!-- Theme -->
<StackPanel Spacing="6">
<TextBlock Text="THEME" Classes="label" />
<ComboBox ItemsSource="{Binding ThemeLabels}"
SelectedIndex="{Binding SelectedThemeIndex, Mode=TwoWay}"
HorizontalAlignment="Stretch"
Padding="12,10"
FontSize="14" />
</StackPanel>
<!-- Language -->
<StackPanel Spacing="6">
<TextBlock Text="LANGUAGE" Classes="label" />
<ComboBox ItemsSource="{Binding LanguageLabels}"
SelectedIndex="{Binding SelectedLanguageIndex, Mode=TwoWay}"
HorizontalAlignment="Stretch"
Padding="12,10"
FontSize="14" />
</StackPanel>
<Separator />
<!-- Save button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,12"
IsEnabled="{Binding !IsSaving}"
Command="{Binding SaveProfileCommand}">
<TextBlock Text="{Binding IsSaving, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Saving...|Save Changes'}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</Button>
</StackPanel>
</Border>
<!-- ══════════════════════════════════════
SECTION: Account & Security
══════════════════════════════════════ -->
<TextBlock Text="ACCOUNT &amp; SECURITY" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="0"
Margin="0,0,0,20">
<StackPanel Spacing="0">
<!-- ── Email row ──────────────────────── -->
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1"
Padding="16,0">
<Panel>
<!-- Display row -->
<Grid IsVisible="{Binding !IsChangingEmail}"
ColumnDefinitions="*,Auto"
MinHeight="56">
<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"
VerticalAlignment="Center"
Command="{Binding StartChangeEmailCommand}">
<StackPanel Orientation="Horizontal" Spacing="4">
<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" />
<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>
<StackPanel Spacing="6">
<TextBlock Text="NEW EMAIL" Classes="label" />
<TextBox Text="{Binding NewEmail, Mode=TwoWay}"
Watermark="new@email.com"
FontSize="14"
Height="44"
Padding="12,0"
VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Text="CONFIRM WITH PASSWORD" Classes="label" />
<TextBox Text="{Binding EmailConfirmPassword, Mode=TwoWay}"
Watermark="Current password"
PasswordChar="•"
FontSize="14"
Height="44"
Padding="12,0"
VerticalContentAlignment="Center" />
</StackPanel>
<Grid ColumnDefinitions="*,*" ColumnSpacing="8">
<Button Grid.Column="0"
Classes="base"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,11"
FontSize="13"
Content="Cancel"
Command="{Binding CancelChangeEmailCommand}" />
<Button Grid.Column="1"
Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,11"
IsEnabled="{Binding !IsSaving}"
Command="{Binding ConfirmChangeEmailCommand}">
<TextBlock Text="Update Email"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}" />
</Button>
</Grid>
</StackPanel>
</Panel>
</Border>
<!-- ── Password row ───────────────────── -->
<Border Padding="16,0">
<Panel>
<!-- Display row -->
<Grid IsVisible="{Binding !IsChangingPassword}"
ColumnDefinitions="*,Auto"
MinHeight="56">
<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"
VerticalAlignment="Center"
Command="{Binding StartChangePasswordCommand}">
<StackPanel Orientation="Horizontal" Spacing="4">
<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" />
<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="14"
Height="44"
Padding="12,0"
VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Text="NEW PASSWORD" Classes="label" />
<TextBox Text="{Binding NewPassword, Mode=TwoWay}"
Watermark="Min. 8 characters"
PasswordChar="•"
FontSize="14"
Height="44"
Padding="12,0"
VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Text="CONFIRM NEW PASSWORD" Classes="label" />
<TextBox Text="{Binding ConfirmNewPassword, Mode=TwoWay}"
Watermark="Repeat new password"
PasswordChar="•"
FontSize="14"
Height="44"
Padding="12,0"
VerticalContentAlignment="Center" />
</StackPanel>
<Grid ColumnDefinitions="*,*" ColumnSpacing="8">
<Button Grid.Column="0"
Classes="base"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,11"
FontSize="13"
Content="Cancel"
Command="{Binding CancelChangePasswordCommand}" />
<Button Grid.Column="1"
Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,11"
IsEnabled="{Binding !IsSaving}"
Command="{Binding ConfirmChangePasswordCommand}">
<TextBlock Text="Update Password"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}" />
</Button>
</Grid>
</StackPanel>
</Panel>
</Border>
</StackPanel>
</Border>
<!-- ══════════════════════════════════════
SECTION: Session
══════════════════════════════════════ -->
<TextBlock Text="SESSION" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="16">
<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"
Classes="danger"
Padding="14,10"
VerticalAlignment="Center"
Command="{Binding SignOutCommand}">
<StackPanel Orientation="Horizontal" Spacing="7">
<Svg Path="../Assets/Icons/log-out.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Sign Out" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.MobileViews;
public partial class SettingsViewMobile : UserControl
{
public SettingsViewMobile()
{
InitializeComponent();
}
}

View File

@@ -4,6 +4,7 @@
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" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.TransactionFormViewMobile"
x:DataType="vm:TransactionFormViewModel"
@@ -53,8 +54,8 @@
<!-- Delete (edit mode only) -->
<Button Grid.Column="2"
Background="#1A0808"
BorderBrush="#3A1515"
Background="{DynamicResource DangerButtonBackground}"
BorderBrush="{DynamicResource DangerButtonBorder}"
BorderThickness="1"
CornerRadius="20"
Width="36" Height="36"
@@ -88,7 +89,7 @@
Padding="3"
Margin="0,0,0,20"
Height="50">
<Grid ColumnDefinitions="*,*">
<Grid ColumnDefinitions="*,*,*">
<!-- Expense -->
<Button Grid.Column="0"
Classes="nav"
@@ -101,11 +102,11 @@
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="expense">
<StackPanel Orientation="Horizontal" Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="5">
<Svg Path="../Assets/Icons/arrow-up-right.svg"
Width="16" Height="16" />
Width="14" Height="14" />
<TextBlock Text="Expense"
FontSize="16"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center" />
</StackPanel>
@@ -122,11 +123,31 @@
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="income">
<StackPanel Orientation="Horizontal" Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="5">
<Svg Path="../Assets/Icons/arrow-down-left.svg"
Width="16" Height="16" />
Width="14" Height="14" />
<TextBlock Text="Income"
FontSize="16"
FontSize="14"
VerticalAlignment="Center" />
</StackPanel>
</Button>
<!-- Transfer -->
<Button Grid.Column="2"
Classes="nav"
Classes.accented="{Binding IsTransfer}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalAlignment="Stretch"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="transfer">
<StackPanel Orientation="Horizontal" Spacing="5">
<Svg Path="../Assets/Icons/arrow-right-left.svg"
Width="14" Height="14" />
<TextBlock Text="Transfer"
FontSize="14"
VerticalAlignment="Center" />
</StackPanel>
</Button>
@@ -144,7 +165,7 @@
Height="64">
<Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0"
Text="$"
Text="{Binding CurrencySymbol}"
FontSize="32"
FontWeight="Bold"
Foreground="{DynamicResource TextMuted}"
@@ -152,7 +173,7 @@
Margin="0,0,8,0"
Padding="0 0 0 2" />
<TextBox Grid.Column="1"
Classes="ghost numeric"
Classes="ghost"
Text="{Binding Amount, Mode=TwoWay}"
Watermark="0.00"
FontSize="32"
@@ -161,6 +182,9 @@
Height="54"
Padding="0 0 0 2"
VerticalContentAlignment="Center">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
<TextBlock Grid.Column="2"
Text="{Binding Currency}"
@@ -171,18 +195,21 @@
</Grid>
</Border>
<!-- ── Description ─────────────────── -->
<TextBlock Text="DESCRIPTION" Classes="label" FontSize="14" Margin="0,0,0,6" />
<!-- ── Description (hidden for transfers) ─── -->
<TextBlock Text="DESCRIPTION" Classes="label" FontSize="14" Margin="0,0,0,6"
IsVisible="{Binding !IsTransfer}" />
<TextBox Text="{Binding Description, Mode=TwoWay}"
Watermark="e.g. Grocery Shopping"
FontSize="16"
Height="48"
Padding="12,0"
VerticalContentAlignment="Center"
Margin="0,0,0,16" />
Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}" />
<!-- ── Category + Account ──────────── -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- ── Category + Account (income/expense) ── -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}">
<!-- Category -->
<StackPanel Grid.Column="0" Spacing="6">
@@ -260,6 +287,143 @@
</Grid>
<!-- ── From + To accounts (transfer) ── -->
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
IsVisible="{Binding IsTransfer}">
<!-- From Account -->
<StackPanel Grid.Column="0" Spacing="6">
<TextBlock Text="FROM ACCOUNT" Classes="label" FontSize="12" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}">
<Grid ColumnDefinitions="Auto,*" Height="42">
<Border Grid.Column="0"
CornerRadius="7"
Width="30" Height="30"
Margin="6,0,0,0"
VerticalAlignment="Center">
<Border.Background>
<SolidColorBrush
Color="{Binding SelectedAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding SelectedAccount.Icon, Converter={StaticResource SvgPathFromName}}"
Width="13" Height="13"
Css="{Binding SelectedAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Accounts}"
SelectedItem="{Binding SelectedAccount, Mode=TwoWay}"
DisplayMemberBinding="{Binding Name}"
Background="Transparent"
BorderThickness="0"
Padding="6,8"
FontSize="12"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
</StackPanel>
<!-- Arrow -->
<Svg Grid.Column="1"
Path="../Assets/Icons/arrow-right.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Bottom"
Margin="6,0,6,12" />
<!-- To Account -->
<StackPanel Grid.Column="2" Spacing="6">
<TextBlock Text="TO ACCOUNT" Classes="label" FontSize="12" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}">
<Grid ColumnDefinitions="Auto,*" Height="42">
<Border Grid.Column="0"
CornerRadius="7"
Width="30" Height="30"
Margin="6,0,0,0"
VerticalAlignment="Center">
<Border.Background>
<SolidColorBrush
Color="{Binding SelectedToAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding SelectedToAccount.Icon, Converter={StaticResource SvgPathFromName}}"
Width="13" Height="13"
Css="{Binding SelectedToAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Accounts}"
SelectedItem="{Binding SelectedToAccount, Mode=TwoWay}"
DisplayMemberBinding="{Binding Name}"
Background="Transparent"
BorderThickness="0"
Padding="6,8"
FontSize="12"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
</StackPanel>
</Grid>
<!-- ── Exchange Rate ──────────────────── -->
<Border IsVisible="{Binding ShowExchangeRateField}"
Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="14,10"
Margin="0,0,0,16">
<StackPanel Spacing="6">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="EXCHANGE RATE"
Classes="label"
VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
IsVisible="{Binding IsFetchingRate}"
VerticalAlignment="Center">
<Border Width="8" Height="8"
CornerRadius="4"
Background="{DynamicResource AccentBlue}"
Opacity="0.7" />
<TextBlock Text="Fetching rate..."
FontSize="11"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Grid>
<TextBlock Text="{Binding ExchangeRateLabel}"
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
<Border IsVisible="{Binding IsFetchingRate}"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Height="36"
Opacity="0.5" />
<Border IsVisible="{Binding !IsFetchingRate}"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="12,0">
<TextBox Classes="ghost"
Text="{Binding ExchangeRate, Mode=TwoWay}"
Watermark="0.00000"
FontSize="13"
Height="36"
Padding="0"
VerticalContentAlignment="Center">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
</Border>
</StackPanel>
</Border>
<!-- ── Date ────────────────────────── -->
<TextBlock Text="DATE" Classes="label" FontSize="14" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
@@ -302,6 +466,46 @@
VerticalContentAlignment="Center"
Margin="0,0,0,8" />
<!-- ── Budget approaching warning ──────── -->
<Border Background="{DynamicResource BadgeBgYellow}"
BorderBrush="{DynamicResource AccentYellow}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,8,0,8"
IsVisible="{Binding HasBudgetApproachingWarning}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/triangle-alert.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #F5C842; }" />
<TextBlock Text="{Binding BudgetWarningMessage}"
FontSize="12"
Foreground="{DynamicResource AccentYellow}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- ── Budget over-limit warning ──────── -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,8,0,8"
IsVisible="{Binding BudgetWarningIsOverBudget}">
<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 BudgetWarningMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- ── Validation error ─────────────── -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
@@ -370,8 +574,7 @@
BorderThickness="1"
CornerRadius="18"
Padding="28"
Width="340"
BoxShadow="0 24 72 0 #60000000">
Width="340">
<StackPanel Spacing="0">
<!-- Icon -->

View File

@@ -7,6 +7,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Clario.MobileViews.TransactionsViewMobile"
x:DataType="vm:TransactionsViewModel"
x:Name="transactionsRoot"
Classes="mobile">
<Grid RowDefinitions="Auto,Auto,*,Auto"
Background="{DynamicResource BgBase}">
@@ -37,8 +38,7 @@
BorderThickness="1"
CornerRadius="16"
Padding="20"
Width="300"
BoxShadow="0 8 32 0 #3C000000">
Width="300">
<StackPanel Spacing="0">
<TextBlock Text="FILTERS"
@@ -71,7 +71,7 @@
CornerRadius="{DynamicResource RadiusControl}"
Padding="3"
Margin="0,0,0,14">
<Grid ColumnDefinitions="*,*,*">
<Grid ColumnDefinitions="*,*,*,*">
<Button Grid.Column="0"
Classes="nav"
Classes.accented="{Binding FilterTypeAll}"
@@ -108,6 +108,18 @@
CommandParameter="expense">
<TextBlock Text="Expense" FontSize="12" HorizontalAlignment="Center" />
</Button>
<Button Grid.Column="3"
Classes="nav"
Classes.accented="{Binding FilterTypeTransfer}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Focusable="False"
CornerRadius="7"
Padding="0,6"
Command="{Binding SetTransactionTypeCommand}"
CommandParameter="transfer">
<TextBlock Text="Transfer" FontSize="12" HorizontalAlignment="Center" />
</Button>
</Grid>
</Border>
@@ -282,69 +294,98 @@
Classes="label" />
</Border>
<!-- Transaction row -->
<Border Padding="16,12"
IsVisible="{Binding !GroupHeader}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Transaction row (tappable to edit) -->
<Button IsVisible="{Binding !GroupHeader}"
Padding="0"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Focusable="False"
Command="{Binding DataContext.EditTransactionCommand, ElementName=transactionsRoot}"
CommandParameter="{Binding .}">
<Border Padding="16,12"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,0">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Icon -->
<Border Grid.Column="0"
CornerRadius="{DynamicResource RadiusIcon}"
Width="38" Height="38"
VerticalAlignment="Center"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Width="17" Height="17"
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<!-- Icon -->
<Panel Grid.Column="0"
Width="38" Height="38"
VerticalAlignment="Center"
Margin="0,0,12,0">
<!-- Category icon -->
<Border CornerRadius="{DynamicResource RadiusIcon}"
Width="38" Height="38"
IsVisible="{Binding !IsTransfer}">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Width="17" Height="17"
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<!-- Transfer icon -->
<Border CornerRadius="{DynamicResource RadiusIcon}"
Width="38" Height="38"
Background="{DynamicResource IconBgBlue}"
IsVisible="{Binding IsTransfer}">
<Svg Path="../Assets/Icons/arrow-right-left.svg"
Width="17" Height="17"
Css="{DynamicResource SvgBlue}" />
</Border>
</Panel>
<!-- Description + meta -->
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="2">
<TextBlock Text="{Binding Description}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding Category.Name}"
FontSize="11"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="·"
FontSize="11"
Foreground="{DynamicResource TextDisabled}" />
<TextBlock Text="{Binding Date, Converter={StaticResource DateFormatConverter}}"
<!-- Description + meta -->
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="2">
<TextBlock Text="{Binding Description}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<StackPanel Orientation="Horizontal" Spacing="6">
<!-- Normal category name -->
<TextBlock Text="{Binding Category.Name}"
FontSize="11"
IsVisible="{Binding !IsTransfer}"
Foreground="{DynamicResource TextMuted}" />
<!-- Transfer label -->
<TextBlock Text="Transfer"
FontSize="11"
IsVisible="{Binding IsTransfer}"
Foreground="{DynamicResource AccentBlue}" />
<TextBlock Text="·"
FontSize="11"
Foreground="{DynamicResource TextDisabled}" />
<TextBlock Text="{Binding Date, Converter={StaticResource DateFormatConverter}}"
FontSize="11"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</StackPanel>
<!-- Dual-currency amount display -->
<StackPanel Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding PrimaryAmountFormatted}"
FontSize="14"
FontWeight="SemiBold"
HorizontalAlignment="Right"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" />
<TextBlock Text="{Binding OriginalAmountFormatted}"
FontSize="11"
HorizontalAlignment="Right"
IsVisible="{Binding IsMultiCurrency}"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</StackPanel>
<!-- Dual-currency amount display -->
<StackPanel Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding PrimaryAmountFormatted}"
FontSize="14"
FontWeight="SemiBold"
HorizontalAlignment="Right"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" />
<TextBlock Text="{Binding OriginalAmountFormatted}"
FontSize="11"
HorizontalAlignment="Right"
IsVisible="{Binding IsMultiCurrency}"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</Button>
</Panel>
</DataTemplate>

View File

@@ -0,0 +1,12 @@
namespace Clario.Models;
public class CategorySpendRow
{
public string Name { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public string Color { get; set; } = "#7B9CFF";
public decimal Amount { get; set; }
public double Percentage { get; set; }
public string AmountFormatted { get; set; } = string.Empty;
public string PercentageFormatted => $"{Percentage:F1}%";
}

View File

@@ -35,10 +35,13 @@ public class Transaction : BaseModel
[Column("exchange_rate")] public decimal? ExchangeRate { get; set; }
[Column("transfer_pair_id")] public Guid? TransferPairId { get; set; }
// Set during enrichment by GeneralDataRepo.LinkTransactionAccounts
[JsonIgnore] public string AccountCurrency { get; set; } = "";
[JsonIgnore] public string PrimaryAmountFormatted { get; set; } = "";
[JsonIgnore] public string OriginalAmountFormatted { get; set; } = "";
[JsonIgnore] public string AccountDisplayText { get; set; } = "";
[JsonIgnore] public decimal ConvertedAmount =>
!string.IsNullOrEmpty(AccountCurrency) && CurrencyService.LiveRates.TryGetValue(AccountCurrency, out var liveRate)
@@ -48,5 +51,7 @@ public class Transaction : BaseModel
[JsonIgnore] public string PrimaryAmountSignFormatted =>
Type == "expense" ? $"-{PrimaryAmountFormatted}" : $"+{PrimaryAmountFormatted}";
[JsonIgnore] public bool IsTransfer => Type is "transfer_in" or "transfer_out";
[JsonIgnore] public bool IsTransferOut => Type == "transfer_out";
[JsonIgnore] public bool GroupHeader { get; set; } = false;
}

View File

@@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Clario.Data;
using Clario.Models;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace Clario.Services;
public static class PdfExportService
{
// ── Print palette (readable on white paper) ───────────
private const string TextPrimary = "#111827";
private const string TextSecondary = "#374151";
private const string TextMuted = "#6B7280";
private const string Border = "#E5E7EB";
private const string HeaderBg = "#F3F4F6";
private const string IncomeColor = "#15803D";
private const string ExpenseColor = "#B91C1C";
private const string BlueColor = "#1D4ED8";
private const string AccentBar = "#1D4ED8"; // header rule
static PdfExportService()
{
QuestPDF.Settings.License = LicenseType.Community;
}
public static async Task<string?> ExportAsync(
GeneralDataRepo data,
DateTime start,
DateTime end,
string periodLabel,
List<CategorySpendRow> topCategories)
{
var culture = new CultureInfo("en-US");
var sym = CurrencyService.GetSymbol(data.PrimaryAccount?.Currency ?? data.Profile?.Currency ?? "USD");
var userName = data.Profile?.DisplayName ?? "User";
var periodTxs = data.Transactions
.Where(t => !t.IsTransfer && t.Date.Date >= start.Date && t.Date.Date <= end.Date)
.OrderByDescending(t => t.Date)
.ToList();
var totalIncome = periodTxs.Where(t => t.Type == "income").Sum(t => t.ConvertedAmount);
var totalExpenses = periodTxs.Where(t => t.Type == "expense").Sum(t => t.ConvertedAmount);
var net = totalIncome - totalExpenses;
var savingsRate = totalIncome > 0 ? net / totalIncome * 100 : 0;
var subtitle = $"{start.ToString("MMM d, yyyy", culture)} {end.ToString("MMM d, yyyy", culture)}";
var generatedAt = DateTime.Now.ToString("MMM d, yyyy 'at' h:mm tt", culture);
// Load logo on the calling (UI) thread before entering Task.Run
byte[] logoBytes;
using (var assetStream = AssetLoader.Open(new Uri("avares://Clario/Assets/Logo/logo-combined-primary-transparent-384x128.png")))
using (var ms = new MemoryStream())
{
await assetStream.CopyToAsync(ms);
logoBytes = ms.ToArray();
}
byte[] pdfBytes = [];
await Task.Run(() =>
{
pdfBytes = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.MarginHorizontal(1.8f, Unit.Centimetre);
page.MarginVertical(1.5f, Unit.Centimetre);
page.DefaultTextStyle(s => s.FontSize(10).FontColor(TextPrimary).FontFamily("Arial"));
// ── Header ────────────────────────────────────
page.Header().Column(col =>
{
col.Item().Row(row =>
{
row.RelativeItem().Column(left =>
{
left.Item().Height(32).Image(logoBytes).FitHeight();
left.Item().PaddingTop(4).Text($"Financial Report — {periodLabel}")
.FontSize(12).SemiBold().FontColor(TextSecondary);
left.Item().Text(subtitle).FontSize(9).FontColor(TextMuted);
});
row.ConstantItem(140).AlignRight().Column(right =>
{
right.Item().AlignRight().Text(userName)
.FontSize(10).SemiBold().FontColor(TextPrimary);
right.Item().AlignRight().Text($"Generated {generatedAt}")
.FontSize(8).FontColor(TextMuted);
});
});
col.Item().PaddingTop(10).LineHorizontal(2).LineColor(AccentBar);
});
// ── Content ───────────────────────────────────
page.Content().PaddingTop(18).Column(col =>
{
// ─ KPI cards ─────────────────────────────
col.Item().Text("Summary").FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.RelativeColumn();
c.RelativeColumn();
c.RelativeColumn();
c.RelativeColumn();
});
void KpiCell(string label, string value, string valueColor)
{
table.Cell()
.Border(1).BorderColor(Border)
.Background(HeaderBg)
.Padding(12)
.Column(c =>
{
c.Item().Text(label).FontSize(8).FontColor(TextMuted);
c.Item().PaddingTop(4).Text(value)
.FontSize(14).Bold().FontColor(valueColor);
});
}
KpiCell("TOTAL INCOME", $"{sym}{totalIncome:N2}", IncomeColor);
KpiCell("TOTAL EXPENSES", $"{sym}{totalExpenses:N2}", ExpenseColor);
KpiCell("NET SAVINGS", $"{(net >= 0 ? "+" : "")}{sym}{net:N2}",
net >= 0 ? IncomeColor : ExpenseColor);
KpiCell("SAVINGS RATE", totalIncome > 0 ? $"{savingsRate:F1}%" : "—", BlueColor);
});
col.Item().Height(20);
// ─ Top categories ─────────────────────────
if (topCategories.Count > 0)
{
col.Item().Text("Top Spending Categories")
.FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.RelativeColumn(4);
c.RelativeColumn(2);
c.RelativeColumn(1);
});
// Header row — CATEGORY stays left, rest centered
void TH(string text, bool leftAlign = false)
{
var cell = table.Cell().Background(HeaderBg)
.PaddingHorizontal(8).PaddingVertical(7)
.BorderBottom(1).BorderColor(Border);
if (leftAlign)
cell.Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
else
cell.AlignCenter().Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
}
TH("CATEGORY", leftAlign: true); TH("AMOUNT"); TH("SHARE");
foreach (var cat in topCategories)
{
table.Cell().PaddingHorizontal(8).PaddingVertical(7)
.BorderBottom(1).BorderColor(Border)
.Text(cat.Name).FontSize(10).FontColor(TextPrimary);
table.Cell().PaddingHorizontal(8).PaddingVertical(7)
.BorderBottom(1).BorderColor(Border)
.Text(cat.AmountFormatted).FontSize(10).FontColor(TextSecondary);
table.Cell().PaddingHorizontal(8).PaddingVertical(7)
.BorderBottom(1).BorderColor(Border)
.AlignRight()
.Text(cat.PercentageFormatted).FontSize(10).FontColor(TextMuted);
}
});
col.Item().Height(20);
}
// ─ Transactions ───────────────────────────
col.Item().Text($"Transactions ({periodTxs.Count})")
.FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(60); // date
c.RelativeColumn(3); // description
c.RelativeColumn(2); // category
c.RelativeColumn(2); // account
c.RelativeColumn(2); // amount
});
// Header — DESCRIPTION stays left, all others centered
void TH(string text, bool leftAlign = false)
{
var cell = table.Cell().Background(HeaderBg)
.PaddingHorizontal(6).PaddingVertical(7)
.BorderBottom(2).BorderColor(Border);
if (leftAlign)
cell.Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
else
cell.AlignCenter().Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
}
TH("DATE"); TH("DESCRIPTION", leftAlign: true); TH("CATEGORY"); TH("ACCOUNT"); TH("AMOUNT");
foreach (var tx in periodTxs)
{
var amountStr = tx.Type == "income"
? $"+{sym}{tx.ConvertedAmount:N2}"
: $"-{sym}{tx.ConvertedAmount:N2}";
var amountColor = tx.Type == "income" ? IncomeColor : ExpenseColor;
void TD(string text, string color = TextPrimary, bool rightAlign = false)
{
var cell = table.Cell()
.PaddingHorizontal(6).PaddingVertical(6)
.BorderBottom(1).BorderColor(Border);
if (rightAlign)
cell.AlignRight().Text(text).FontSize(9).FontColor(color);
else
cell.Text(text).FontSize(9).FontColor(color);
}
TD(tx.Date.ToString("MMM d, yy", culture), TextMuted);
TD(tx.Description);
TD(tx.Category?.Name ?? "—", TextSecondary);
TD(tx.AccountDisplayText, TextSecondary);
TD(amountStr, amountColor, rightAlign: true);
}
});
});
// ── Footer ────────────────────────────────────
page.Footer().PaddingTop(6).BorderTop(1).BorderColor(Border).Row(row =>
{
row.RelativeItem().Text("Generated by Clario — Your personal finance tracker")
.FontSize(8).FontColor(TextMuted);
row.ConstantItem(60).AlignRight().Text(t =>
{
t.AlignRight();
t.Span("Page ").FontSize(8).FontColor(TextMuted);
t.CurrentPageNumber().FontSize(8).FontColor(TextMuted);
t.Span(" of ").FontSize(8).FontColor(TextMuted);
t.TotalPages().FontSize(8).FontColor(TextMuted);
});
});
});
}).GeneratePdf();
});
return await SavePdfAsync(pdfBytes, $"Clario_Report_{DateTime.Now:yyyy-MM-dd}.pdf");
}
private static async Task<string?> SavePdfAsync(byte[] pdfBytes, string suggestedName)
{
var topLevel = GetTopLevel();
if (topLevel is null)
{
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), suggestedName);
await File.WriteAllBytesAsync(path, pdfBytes);
return path;
}
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Save PDF Report",
SuggestedFileName = suggestedName,
FileTypeChoices = [new FilePickerFileType("PDF Document") { Patterns = ["*.pdf"] }]
});
if (file is null) return null;
await using var stream = await file.OpenWriteAsync();
await stream.WriteAsync(pdfBytes);
return file.Path.LocalPath;
}
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;
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
@@ -21,37 +22,56 @@ public partial class AccountsViewModel : ViewModelBase
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private Account? _selectedAccount;
[ObservableProperty] private bool _isAccountDeletionConfirmationVisible;
public bool CanDeleteAccount => VisibleAccounts.Count > 1;
public bool CanDeleteAccount => VisibleAccounts.Count(x => !x.GroupHeader) > 1;
public int ActiveAccountCount => VisibleAccounts.Count(x => !x.GroupHeader);
[ObservableProperty] private bool _isDeleteDialogVisible;
[ObservableProperty] private DeleteAccountDialogViewModel _deleteDialog = new();
[ObservableProperty] private bool _isArchiveDialogVisible;
[ObservableProperty] private Account? _accountToArchive;
[ObservableProperty] private bool _isArchivedListVisible;
[ObservableProperty] private List<Account> _archivedAccounts = new();
public bool HasArchivedAccounts => ArchivedAccounts.Count > 0;
public AccountsViewModel()
{
AppData.Accounts.CollectionChanged += (_, _) => { Initialize(); };
AppData.Transactions.CollectionChanged += (_, _) => { Initialize(); };
Initialize();
}
public void Initialize()
{
var prevSelectedId = SelectedAccount?.Id;
FetchAndProcessAccountInfo();
GroupAccounts();
SelectedAccount = VisibleAccounts.FirstOrDefault(x => !x.GroupHeader);
ArchivedAccounts = AppData.Accounts.Where(a => a.IsArchived).ToList();
OnPropertyChanged(nameof(HasArchivedAccounts));
OnPropertyChanged(nameof(ActiveAccountCount));
OnPropertyChanged(nameof(CanDeleteAccount));
// Set to null first so PropertyChanged fires even when re-selecting the same account,
// ensuring the detail panel re-reads all computed properties (balance, income, etc.)
SelectedAccount = null;
SelectedAccount = (prevSelectedId.HasValue
? VisibleAccounts.FirstOrDefault(a => a.Id == prevSelectedId && !a.GroupHeader)
: null) ?? VisibleAccounts.FirstOrDefault(x => !x.GroupHeader);
}
private void FetchAndProcessAccountInfo()
{
TotalBalance = 0;
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
foreach (var account in AppData.Accounts)
foreach (var account in AppData.Accounts.Where(a => !a.IsArchived))
{
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);
account.TotalExpenseThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type == "expense").Sum(t => t.Amount);
account.IncomeTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type == "income");
account.ExpenseTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type == "expense");
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.Amount : -t.Amount);
account.TotalIncomeThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type is "income" or "transfer_in").Sum(t => t.Amount);
account.TotalExpenseThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type is "expense" or "transfer_out").Sum(t => t.Amount);
account.IncomeTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type is "income" or "transfer_in");
account.ExpenseTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type is "expense" or "transfer_out");
account.RecentTransactions = accountTransactions.OrderByDescending(t => t.Date).Take(3).ToList();
var lastMonthBalance = accountTransactions.Where(t => t.Date.Month == DateTime.Now.AddMonths(-1).Month && t.Type == "income")
.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
@@ -59,7 +79,7 @@ public partial class AccountsViewModel : ViewModelBase
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
TotalBalance += account.CurrentBalance;
else
TotalBalance += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
TotalBalance += accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.ConvertedAmount : -t.ConvertedAmount);
}
}
@@ -91,7 +111,7 @@ public partial class AccountsViewModel : ViewModelBase
foreach (var type in accountTypes)
{
var accountsOfType = AppData.Accounts
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase))
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase) && !a.IsArchived)
.OrderByDescending(a => a.IsPrimary)
.ThenBy(a => a.CreatedAt)
.ToList();
@@ -109,6 +129,53 @@ public partial class AccountsViewModel : ViewModelBase
OnPropertyChanged(nameof(CanDeleteAccount));
}
[RelayCommand]
private void RequestArchiveAccount(Account account)
{
AccountToArchive = account;
IsArchiveDialogVisible = true;
}
[RelayCommand]
private void CancelArchive()
{
IsArchiveDialogVisible = false;
AccountToArchive = null;
}
[RelayCommand]
private async Task ConfirmArchive()
{
if (AccountToArchive is null) return;
AccountToArchive.IsArchived = true;
await AppData.UpdateAccount(AccountToArchive);
IsArchiveDialogVisible = false;
AccountToArchive = null;
Initialize();
}
[RelayCommand]
private void ShowArchivedList()
{
IsArchivedListVisible = true;
}
[RelayCommand]
private void CloseArchivedList()
{
IsArchivedListVisible = false;
}
[RelayCommand]
private async Task UnarchiveAccount(Account account)
{
account.IsArchived = false;
await AppData.UpdateAccount(account);
Initialize();
if (!HasArchivedAccounts)
IsArchivedListVisible = false;
}
[RelayCommand]
private void RequestDeleteAccount(Account account)
{

View File

@@ -0,0 +1,436 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SKColor = SkiaSharp.SKColor;
namespace Clario.ViewModels;
public partial class AnalyticsViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
public GeneralDataRepo AppData => DataRepo.General;
// ── Period ───────────────────────────────────────────
public List<string> PeriodOptions { get; } = new()
{
"Last 30 Days", "Last 3 Months", "Last 6 Months", "Last 12 Months", "This Year"
};
[ObservableProperty] private string _selectedPeriod = "Last 6 Months";
partial void OnSelectedPeriodChanged(string value) => Initialize();
// ── KPI cards ────────────────────────────────────────
[ObservableProperty] private string _totalIncomeFormatted = "—";
[ObservableProperty] private string _totalExpensesFormatted = "—";
[ObservableProperty] private string _netSavingsFormatted = "—";
[ObservableProperty] private string _savingsRateFormatted = "—";
[ObservableProperty] private bool _netSavingsPositive = true;
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
// ── Cash Flow chart ──────────────────────────────────
[ObservableProperty] private ISeries[] _cashFlowSeries = [];
[ObservableProperty] private Axis[] _cashFlowXAxes = [];
[ObservableProperty] private Axis[] _cashFlowYAxes = [];
// ── Net Worth chart ──────────────────────────────────
[ObservableProperty] private ISeries[] _netWorthSeries = [];
[ObservableProperty] private Axis[] _netWorthXAxes = [];
[ObservableProperty] private Axis[] _netWorthYAxes = [];
// ── Day-of-week chart ────────────────────────────────
[ObservableProperty] private ISeries[] _dayOfWeekSeries = [];
[ObservableProperty] private Axis[] _dayOfWeekXAxes = [];
// ── Top categories ───────────────────────────────────
[ObservableProperty] private ObservableCollection<CategorySpendRow> _topCategories = new();
[ObservableProperty] private bool _hasTopCategories;
// ── Income sources donut ─────────────────────────────
[ObservableProperty] private ISeries[] _incomeSourcesSeries = [];
[ObservableProperty] private bool _hasIncomeSources;
// ── State ────────────────────────────────────────────
[ObservableProperty] private bool _isExporting;
[ObservableProperty] private string? _exportStatusMessage;
// ─────────────────────────────────────────────────────
public AnalyticsViewModel()
{
AppData.Transactions.CollectionChanged += (_, _) => Initialize();
AppData.Accounts.CollectionChanged += (_, _) => Initialize();
Initialize();
}
public void Initialize()
{
try
{
var (start, end) = GetDateRange();
var periodTxs = AppData.Transactions
.Where(t => !t.IsTransfer && t.Date.Date >= start.Date && t.Date.Date <= end.Date)
.ToList();
var expenses = periodTxs.Where(t => t.Type == "expense").ToList();
var income = periodTxs.Where(t => t.Type == "income").ToList();
ComputeKpis(income, expenses);
BuildCashFlowChart(start, end);
BuildNetWorthChart(start, end);
BuildDayOfWeekChart(expenses, start, end);
BuildTopCategories(expenses);
BuildIncomeSourcesChart(income);
OnPropertyChanged(nameof(PrimarySymbol));
}
catch (Exception e)
{
DebugLogger.Log(e);
}
}
// ── Date range ────────────────────────────────────────
private (DateTime start, DateTime end) GetDateRange()
{
var now = DateTime.Now;
return SelectedPeriod switch
{
"Last 30 Days" => (now.AddDays(-30), now),
"Last 3 Months" => (now.AddMonths(-3), now),
"Last 6 Months" => (now.AddMonths(-6), now),
"Last 12 Months" => (now.AddMonths(-12), now),
"This Year" => (new DateTime(now.Year, 1, 1), now),
_ => (now.AddMonths(-6), now)
};
}
private static List<(DateTime monthStart, DateTime monthEnd, string label)> GetMonthBuckets(DateTime start, DateTime end)
{
var buckets = new List<(DateTime, DateTime, string)>();
var current = new DateTime(start.Year, start.Month, 1);
var endMonth = new DateTime(end.Year, end.Month, 1);
var culture = new CultureInfo("en-US");
while (current <= endMonth)
{
var next = current.AddMonths(1);
buckets.Add((current, next.AddSeconds(-1), current.ToString("MMM ''yy", culture)));
current = next;
}
return buckets;
}
// ── Section 1: KPIs ───────────────────────────────────
private void ComputeKpis(List<Transaction> income, List<Transaction> expenses)
{
var sym = PrimarySymbol;
var totalIncome = income.Sum(t => t.ConvertedAmount);
var totalExpenses = expenses.Sum(t => t.ConvertedAmount);
var net = totalIncome - totalExpenses;
TotalIncomeFormatted = $"{sym}{totalIncome:N2}";
TotalExpensesFormatted = $"{sym}{totalExpenses:N2}";
NetSavingsPositive = net >= 0;
NetSavingsFormatted = $"{(net >= 0 ? "+" : "")}{sym}{net:N2}";
SavingsRateFormatted = totalIncome > 0
? $"{Math.Max(0, (net / totalIncome) * 100):F1}%"
: "—";
}
// ── Section 2: Cash Flow ──────────────────────────────
private void BuildCashFlowChart(DateTime start, DateTime end)
{
var buckets = GetMonthBuckets(start, end);
var incomeVals = new double[buckets.Count];
var expenseVals = new double[buckets.Count];
for (var i = 0; i < buckets.Count; i++)
{
var (mStart, mEnd, _) = buckets[i];
incomeVals[i] = (double)AppData.Transactions
.Where(t => t.Type == "income" && t.Date.Date >= mStart && t.Date.Date <= mEnd)
.Sum(t => t.ConvertedAmount);
expenseVals[i] = (double)AppData.Transactions
.Where(t => t.Type == "expense" && t.Date.Date >= mStart && t.Date.Date <= mEnd)
.Sum(t => t.ConvertedAmount);
}
var labels = buckets.Select(b => b.label).ToArray();
CashFlowSeries =
[
new LineSeries<double>
{
Name = "Income",
Values = incomeVals,
Stroke = new SolidColorPaint(SKColor.Parse("#2ECC8A"), 2),
Fill = null,
GeometryFill = new SolidColorPaint(SKColor.Parse("#2ECC8A")),
GeometryStroke = null,
GeometrySize = 5,
LineSmoothness = 0.5
},
new LineSeries<double>
{
Name = "Expenses",
Values = expenseVals,
Stroke = new SolidColorPaint(SKColor.Parse("#FF5E5E"), 2),
Fill = null,
GeometryFill = new SolidColorPaint(SKColor.Parse("#FF5E5E")),
GeometryStroke = null,
GeometrySize = 5,
LineSmoothness = 0.5
}
];
CashFlowXAxes =
[
new Axis
{
Labels = labels,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 11
}
];
var sym = PrimarySymbol;
CashFlowYAxes =
[
new Axis
{
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 10,
Labeler = v => $"{sym}{v:N0}"
}
];
}
// ── Section 3: Net Worth ──────────────────────────────
private void BuildNetWorthChart(DateTime start, DateTime end)
{
// Start from 12 months before start to show history, but respect the selected range
var buckets = GetMonthBuckets(start, end);
var netWorthVals = new double[buckets.Count];
for (var i = 0; i < buckets.Count; i++)
{
var (_, mEnd, _) = buckets[i];
double nw = 0;
foreach (var account in AppData.Accounts.Where(a => !a.IsArchived))
{
var txUpTo = AppData.Transactions.Where(t => t.AccountId == account.Id && t.Date.Date <= mEnd.Date);
nw += (double)(account.OpeningBalance +
txUpTo.Sum(t => t.Type is "income" or "transfer_in" ? t.ConvertedAmount : -t.ConvertedAmount));
}
netWorthVals[i] = nw;
}
var labels = buckets.Select(b => b.label).ToArray();
NetWorthSeries =
[
new LineSeries<double>
{
Name = "Net Worth",
Values = netWorthVals,
Stroke = new SolidColorPaint(SKColor.Parse("#7B9CFF"), 2),
Fill = new SolidColorPaint(SKColor.Parse("#7B9CFF").WithAlpha(25)),
GeometryFill = new SolidColorPaint(SKColor.Parse("#7B9CFF")),
GeometryStroke = null,
GeometrySize = 5,
LineSmoothness = 0.5
}
];
NetWorthXAxes =
[
new Axis
{
Labels = labels,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 11
}
];
var sym = PrimarySymbol;
NetWorthYAxes =
[
new Axis
{
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 10,
Labeler = v => $"{sym}{v:N0}"
}
];
}
// ── Section 4: Day of Week ────────────────────────────
private void BuildDayOfWeekChart(List<Transaction> expenses, DateTime start, DateTime end)
{
// DayOfWeek: Sunday=0, Monday=1 ... Saturday=6
// We display MonSun (index 06 in our array)
var dayLabels = new[] { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
var totals = new double[7];
var counts = new int[7];
// Count occurrences of each weekday in the period
var d = start.Date;
while (d <= end.Date)
{
var idx = ((int)d.DayOfWeek + 6) % 7; // Mon=0..Sun=6
counts[idx]++;
d = d.AddDays(1);
}
foreach (var tx in expenses)
{
var idx = ((int)tx.Date.DayOfWeek + 6) % 7;
totals[idx] += (double)tx.ConvertedAmount;
}
var averages = totals.Select((total, i) => counts[i] > 0 ? total / counts[i] : 0).ToArray();
DayOfWeekSeries =
[
new ColumnSeries<double>
{
Name = "Avg Daily Spend",
Values = averages,
Fill = new SolidColorPaint(SKColor.Parse("#9B7BFF")),
MaxBarWidth = 40,
Padding = 3
}
];
DayOfWeekXAxes =
[
new Axis
{
Labels = dayLabels,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
SeparatorsPaint = null,
TicksPaint = null,
TextSize = 11
}
];
}
// ── Section 5: Top Categories ─────────────────────────
private void BuildTopCategories(List<Transaction> expenses)
{
var sym = PrimarySymbol;
var totalSpend = expenses.Sum(t => t.ConvertedAmount);
if (totalSpend == 0)
{
TopCategories = new ObservableCollection<CategorySpendRow>();
HasTopCategories = false;
return;
}
var grouped = expenses
.Where(t => t.Category is not null)
.GroupBy(t => t.Category!)
.Select(g => new CategorySpendRow
{
Name = g.Key.Name,
Icon = g.Key.Icon,
Color = g.Key.Color,
Amount = g.Sum(t => t.ConvertedAmount),
Percentage = (double)(g.Sum(t => t.ConvertedAmount) / totalSpend * 100),
AmountFormatted = $"{sym}{g.Sum(t => t.ConvertedAmount):N2}"
})
.OrderByDescending(r => r.Amount)
.Take(8)
.ToList();
TopCategories = new ObservableCollection<CategorySpendRow>(grouped);
HasTopCategories = grouped.Count > 0;
}
// ── Section 6: Income Sources ─────────────────────────
private void BuildIncomeSourcesChart(List<Transaction> income)
{
var grouped = income
.Where(t => t.Category is not null)
.GroupBy(t => t.Category!)
.Select(g => (category: g.Key, total: g.Sum(t => t.ConvertedAmount)))
.OrderByDescending(x => x.total)
.ToList();
if (grouped.Count < 2)
{
IncomeSourcesSeries = [];
HasIncomeSources = false;
return;
}
var sym = PrimarySymbol;
IncomeSourcesSeries = grouped.Select(x => (ISeries)new PieSeries<double>
{
Name = x.category.Name,
Values = new[] { (double)x.total },
Fill = new SolidColorPaint(SKColor.Parse(x.category.Color)),
InnerRadius = 20,
ToolTipLabelFormatter = p => $"{sym}{p.Coordinate.PrimaryValue:N2}"
}).ToArray();
HasIncomeSources = true;
}
// ── PDF Export ────────────────────────────────────────
[RelayCommand]
private async Task ExportPdf()
{
if (IsExporting) return;
IsExporting = true;
ExportStatusMessage = null;
try
{
var (start, end) = GetDateRange();
var path = await PdfExportService.ExportAsync(
AppData,
start,
end,
SelectedPeriod,
TopCategories.ToList());
ExportStatusMessage = path is not null ? "PDF saved successfully." : null;
}
catch (Exception e)
{
DebugLogger.Log(e);
ExportStatusMessage = "Export failed. Please try again.";
}
finally
{
IsExporting = false;
}
}
}

View File

@@ -6,11 +6,13 @@ using System.Text.Json;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Clario.Enums;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Supabase.Gotrue;
using Supabase.Gotrue.Exceptions;
namespace Clario.ViewModels;
@@ -36,6 +38,11 @@ public partial class AuthViewModel : ViewModelBase
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
private string _operation = "login";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public AuthViewModel()
{
DebugLogger.Log("auth vm loaded");
@@ -64,11 +71,13 @@ public partial class AuthViewModel : ViewModelBase
private void SetOperation(string operation)
{
Operation = operation;
ErrorMessage = null;
}
[RelayCommand(CanExecute = nameof(canSignin))]
private async Task ConfirmLogin()
{
ErrorMessage = null;
try
{
await SupabaseService.Client.Auth.SignIn(_email, _password);
@@ -84,15 +93,22 @@ public partial class AuthViewModel : ViewModelBase
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
}
}
catch (GotrueException e)
{
DebugLogger.Log(e);
ErrorMessage = GetLoginErrorMessage(e.Reason);
}
catch (Exception e)
{
DebugLogger.Log(e);
ErrorMessage = GetErrorMessage(AuthError.Unknown);
}
}
[RelayCommand(CanExecute = nameof(canCreateAccount))]
private async Task ConfirmCreateAccount()
{
ErrorMessage = null;
try
{
var session = await SupabaseService.Client.Auth.SignUp(
@@ -120,12 +136,52 @@ public partial class AuthViewModel : ViewModelBase
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
}
}
catch (GotrueException e)
{
DebugLogger.Log(e);
ErrorMessage = GetSignupErrorMessage(e.Reason);
}
catch (Exception e)
{
DebugLogger.Log(e);
ErrorMessage = GetErrorMessage(AuthError.Unknown);
}
}
private static string GetLoginErrorMessage(FailureHint.Reason reason) => reason switch
{
FailureHint.Reason.UserBadLogin => GetErrorMessage(AuthError.InvalidCredentials),
FailureHint.Reason.UserBadPassword => GetErrorMessage(AuthError.InvalidCredentials),
FailureHint.Reason.UserEmailNotConfirmed => GetErrorMessage(AuthError.EmailNotConfirmed),
FailureHint.Reason.UserTooManyRequests => GetErrorMessage(AuthError.RateLimited),
FailureHint.Reason.UserBadEmailAddress => GetErrorMessage(AuthError.InvalidEmail),
FailureHint.Reason.Offline => GetErrorMessage(AuthError.Unknown),
_ => GetErrorMessage(AuthError.Unknown),
};
private static string GetSignupErrorMessage(FailureHint.Reason reason) => reason switch
{
FailureHint.Reason.UserAlreadyRegistered => GetErrorMessage(AuthError.EmailAlreadyExists),
FailureHint.Reason.UserBadPassword => GetErrorMessage(AuthError.WeakPassword),
FailureHint.Reason.UserBadEmailAddress => GetErrorMessage(AuthError.InvalidEmail),
FailureHint.Reason.UserTooManyRequests => GetErrorMessage(AuthError.RateLimited),
FailureHint.Reason.Offline => GetErrorMessage(AuthError.Unknown),
_ => GetErrorMessage(AuthError.Unknown),
};
private static string GetErrorMessage(AuthError error) => error switch
{
AuthError.InvalidCredentials => "Invalid email or password.",
AuthError.EmailAlreadyExists => "An account with this email already exists.",
AuthError.EmailNotConfirmed => "Please confirm your email before signing in.",
AuthError.WeakPassword => "Password must be at least 6 characters.",
AuthError.InvalidEmail => "Please enter a valid email address.",
AuthError.SignupDisabled => "Sign-ups are currently disabled.",
AuthError.RateLimited => "Too many attempts. Please wait and try again.",
AuthError.SessionExpired => "Your session has expired. Please sign in again.",
_ => "Something went wrong. Please try again.",
};
public bool isSignin => Operation == "login";
public bool isCreateAccount => Operation == "signup";

View File

@@ -28,8 +28,8 @@ public partial class BudgetViewModel : ViewModelBase
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NextPeriodCommand), nameof(PreviousPeriodCommand))]
private DateTime _currentPeriod = DateTime.Now.Date;
public bool CanGoToNextPeriod => CurrentPeriod.Month < DateTime.Now.Month;
public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && CurrentPeriod.Month > AppData.Transactions.Min(x => x.Date.Month);
public bool CanGoToNextPeriod => CurrentPeriod.Year < DateTime.Now.Year || (CurrentPeriod.Year == DateTime.Now.Year && CurrentPeriod.Month < DateTime.Now.Month);
public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && new DateTime(CurrentPeriod.Year, CurrentPeriod.Month, 1) > new DateTime(AppData.Transactions.Min(x => x.Date).Year, AppData.Transactions.Min(x => x.Date).Month, 1);
public string CurrentPeriodFormatted => CurrentPeriod.ToString("MMMM yyyy");
[ObservableProperty] private ISeries[] _spendingBreakdownChartSeries = [];
@@ -40,7 +40,7 @@ public partial class BudgetViewModel : ViewModelBase
public string SpentPercentageFormatted => (TotalSpent / TotalBudgeted).ToString("P0") + " of total budget.";
public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue);
private string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
public string TotalLeftFormatted => $"{PrimarySymbol}{TotalLeft:N0} left";
public bool HasSavingsGoal => AppData.Profile?.SavingsGoal is > 0;

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Clario.ViewModels;
public partial class CategoryFormViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
// ── Mode ────────────────────────────────────────────────
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel), nameof(CanDelete))]
private bool _isEditMode = false;
public string FormTitle => IsEditMode ? "Edit Category" : "New Category";
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Category";
// ── Fields ──────────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _name = "";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsExpense), nameof(IsIncome))]
private string _type = "expense";
[ObservableProperty] private string _selectedIcon = "utensils";
[ObservableProperty] private string _selectedColor = "#7B9CFF";
// ── Icon options ─────────────────────────────────────────
public List<string> CategoryIcons { get; } = new()
{
// Food & Dining
"utensils", "hamburger", "coffee", "pizza", "wine",
// Shopping
"shopping-cart", "shopping-bag", "package", "gift", "shirt",
// Transport
"car", "bus", "train-front", "bike", "plane",
// Home & Utilities
"house", "zap", "wifi", "plug-2", "wrench",
// Health & Fitness
"heart-pulse", "pill", "dumbbell", "scissors", "stethoscope",
// Entertainment
"gamepad-2", "film", "music", "tv", "headphones",
// Finance
"banknote", "credit-card", "piggy-bank", "wallet", "hand-coins",
"trending-up", "trending-down", "landmark", "circle-dollar-sign", "gem",
// Work & Education
"briefcase", "graduation-cap", "book-open", "target", "mail",
// Personal & Lifestyle
"heart", "moon", "sun", "leaf", "camera",
// Bills & Subscriptions
"receipt", "receipt-text", "smartphone", "volume-2", "refresh-cw",
};
// ── Validation ──────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public bool IsExpense => Type == "expense";
public bool IsIncome => Type == "income";
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
public bool CanDelete => IsEditMode && DataRepo.General.Categories.Count > 4;
// ── Delete confirm sub-modal ────────────────────────────
[ObservableProperty] private bool _showDeleteConfirm = false;
// ── Callbacks ───────────────────────────────────────────
public Action? OnSaved;
public Action? OnCancelled;
public Action? OnDeleted;
// ── Edit mode: original category ────────────────────────
private Guid? _editingId;
// ── Commands ────────────────────────────────────────────
[RelayCommand]
private void SetType(string type) => Type = type;
[RelayCommand]
private void SetIcon(string icon) => SelectedIcon = icon;
[RelayCommand]
private async Task Save()
{
ErrorMessage = null;
if (string.IsNullOrWhiteSpace(Name))
{
ErrorMessage = "Name is required.";
return;
}
try
{
if (IsEditMode && _editingId.HasValue)
{
var updated = new Category
{
Id = _editingId.Value,
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
Name = Name.Trim(),
Type = Type,
Icon = SelectedIcon,
Color = SelectedColor,
};
await DataRepo.General.UpdateCategory(updated);
}
else
{
var category = new Category
{
Id = Guid.NewGuid(),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
Name = Name.Trim(),
Type = Type,
Icon = SelectedIcon,
Color = SelectedColor,
};
await DataRepo.General.InsertCategory(category);
}
OnSaved?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
DebugLogger.Log(ex);
}
}
[RelayCommand]
private void Cancel() => OnCancelled?.Invoke();
[RelayCommand]
private void RequestDelete() => ShowDeleteConfirm = true;
[RelayCommand]
private void CancelDelete() => ShowDeleteConfirm = false;
[RelayCommand]
private async Task ConfirmDelete()
{
if (!IsEditMode || !_editingId.HasValue) return;
try
{
await DataRepo.General.DeleteCategory(_editingId.Value);
OnDeleted?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Failed to delete category.";
DebugLogger.Log(ex);
}
}
// ── Public setup methods ─────────────────────────────────
public void SetupForAdd()
{
ShowDeleteConfirm = false;
IsEditMode = false;
_editingId = null;
Name = "";
Type = "expense";
SelectedIcon = "utensils";
SelectedColor = "#7B9CFF";
ErrorMessage = null;
}
public void SetupForEdit(Category category)
{
ShowDeleteConfirm = false;
IsEditMode = true;
_editingId = category.Id;
Name = category.Name;
Type = category.Type;
SelectedIcon = category.Icon;
SelectedColor = category.Color;
ErrorMessage = null;
OnPropertyChanged(nameof(CanDelete));
}
}

View File

@@ -178,6 +178,12 @@ public partial class DashboardViewModel : ViewModelBase
((MainViewModel)parentViewModel).OpenAddTransaction();
}
[RelayCommand]
private void NavigateToSettings()
{
((MainViewModel)parentViewModel).GoToSettingsCommand.Execute(null);
}
private void UpdateSpendingByCategoryChart(ChartTimePeriod period = ChartTimePeriod.ThisMonth)
{
var tempList = new List<ColumnChartData>();
@@ -241,7 +247,7 @@ public partial class DashboardViewModel : ViewModelBase
private void UpdateRecentTransactions()
{
RecentTransactions = new ObservableCollection<Transaction>(AppData.Transactions.OrderByDescending(x => x.Date).Take(5));
RecentTransactions = new ObservableCollection<Transaction>(AppData.Transactions.Where(x => !x.IsTransfer).OrderByDescending(x => x.Date).Take(5));
OnPropertyChanged(nameof(HasTransactionData));
}
@@ -249,17 +255,17 @@ public partial class DashboardViewModel : ViewModelBase
{
TotalNetworth = 0;
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
foreach (var account in AppData.Accounts)
foreach (var account in AppData.Accounts.Where(a => !a.IsArchived))
{
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);
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.Amount : -t.Amount);
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
TotalNetworth += account.CurrentBalance;
else
TotalNetworth += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
TotalNetworth += accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.ConvertedAmount : -t.ConvertedAmount);
}
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.OrderBy(x => x.CreatedAt));
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.Where(a => !a.IsArchived).OrderBy(x => x.CreatedAt));
OnPropertyChanged(nameof(AccountsSubtitle));
}

View File

@@ -21,6 +21,7 @@ public partial class MainViewModel : ViewModelBase
public TransactionsViewModel _transactionsViewModel = null!;
private AccountsViewModel _accountsViewModel = null!;
private BudgetViewModel _budgetViewModel = null!;
private AnalyticsViewModel _analyticsViewModel = null!;
GeneralDataRepo AppData => DataRepo.General;
[ObservableProperty] private Profile? _profile;
@@ -28,6 +29,7 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private TransactionFormViewModel _transactionFormViewModel = null!;
[ObservableProperty] private AccountFormViewModel _accountFormViewModel = null!;
[ObservableProperty] private BudgetFormViewModel _budgetFormViewModel = null!;
[ObservableProperty] private CategoryFormViewModel _categoryFormViewModel = null!;
[ObservableProperty] private SettingsViewModel _settingsViewModel = null!;
[ObservableProperty] private SetSavingsGoalDialogViewModel _setSavingsGoalDialogViewModel = null!;
@@ -35,11 +37,12 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private bool _isTransactionFormVisible;
[ObservableProperty] private bool _isAccountFormVisible;
[ObservableProperty] private bool _isBudgetFormVisible;
[ObservableProperty] private bool _isCategoryFormVisible;
[ObservableProperty] private bool _isSavingsGoalDialogVisible;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnSettings))]
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnAnalytics), nameof(isOnSettings))]
private ViewModelBase? _currentView;
[ObservableProperty] private bool _isDarkTheme;
@@ -103,6 +106,11 @@ public partial class MainViewModel : ViewModelBase
parentViewModel = this
};
DebugLogger.Log("initialized BudgetViewModel");
_analyticsViewModel = new AnalyticsViewModel()
{
parentViewModel = this
};
DebugLogger.Log("initialized AnalyticsViewModel");
SettingsViewModel = new SettingsViewModel()
{
parentViewModel = this
@@ -112,6 +120,8 @@ public partial class MainViewModel : ViewModelBase
{
parentViewModel = this
};
TransactionFormViewModel.OnOpenCategoryForm = OpenAddCategoryFromTransactionForm;
TransactionFormViewModel.OnOpenEditCategoryForm = OpenEditCategoryFromTransactionForm;
DebugLogger.Log("initialized TransactionFormViewModel");
AccountFormViewModel = new AccountFormViewModel()
{
@@ -123,6 +133,11 @@ public partial class MainViewModel : ViewModelBase
parentViewModel = this
};
DebugLogger.Log("initialized BudgetFormViewModel");
CategoryFormViewModel = new CategoryFormViewModel()
{
parentViewModel = this
};
DebugLogger.Log("initialized CategoryFormViewModel");
SetSavingsGoalDialogViewModel = new SetSavingsGoalDialogViewModel();
DebugLogger.Log("initialized SetSavingsGoalDialogViewModel");
@@ -268,6 +283,78 @@ public partial class MainViewModel : ViewModelBase
IsBudgetFormVisible = false;
}
private void OpenEditCategoryFromTransactionForm(Category category)
{
CategoryFormViewModel.SetupForEdit(category);
CategoryFormViewModel.OnSaved = () =>
{
TransactionFormViewModel.Categories = AppData.Categories;
// Keep the selected category in sync after edit
var updated = AppData.Categories.FirstOrDefault(c => c.Id == category.Id);
if (updated is not null) TransactionFormViewModel.SelectedCategory = updated;
CloseCategoryForm();
};
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
CategoryFormViewModel.OnDeleted = () =>
{
TransactionFormViewModel.Categories = AppData.Categories;
TransactionFormViewModel.SelectedCategory = AppData.Categories.FirstOrDefault(c => c.Type == TransactionFormViewModel.Type);
CloseCategoryForm();
};
IsCategoryFormVisible = true;
}
// Called by the plus button inside TransactionFormView
private void OpenAddCategoryFromTransactionForm()
{
CategoryFormViewModel.SetupForAdd();
CategoryFormViewModel.OnSaved = () =>
{
// Refresh the category list in the transaction form after adding
TransactionFormViewModel.Categories = AppData.Categories;
CloseCategoryForm();
};
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
CategoryFormViewModel.OnDeleted = () =>
{
TransactionFormViewModel.Categories = AppData.Categories;
CloseCategoryForm();
};
IsCategoryFormVisible = true;
}
[RelayCommand]
public void OpenAddCategory()
{
if (IsDimmed) return;
CategoryFormViewModel.SetupForAdd();
CategoryFormViewModel.OnSaved = CloseCategoryForm;
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
CategoryFormViewModel.OnDeleted = CloseCategoryForm;
IsCategoryFormVisible = true;
IsDimmed = true;
}
[RelayCommand]
public void OpenEditCategory(Category category)
{
if (IsDimmed) return;
CategoryFormViewModel.SetupForEdit(category);
CategoryFormViewModel.OnSaved = CloseCategoryForm;
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
CategoryFormViewModel.OnDeleted = CloseCategoryForm;
IsCategoryFormVisible = true;
IsDimmed = true;
}
private void CloseCategoryForm()
{
IsCategoryFormVisible = false;
// Only clear the dim if no other modal is open
if (!IsTransactionFormVisible)
IsDimmed = false;
}
[RelayCommand]
public void OpenEditSavingsGoal()
{
@@ -316,6 +403,12 @@ public partial class MainViewModel : ViewModelBase
CurrentView = _budgetViewModel;
}
[RelayCommand]
private void GoToAnalytics()
{
CurrentView = _analyticsViewModel;
}
[RelayCommand]
private void GoToSettings()
{
@@ -343,5 +436,6 @@ 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 isOnAnalytics => CurrentView is AnalyticsViewModel;
public bool isOnSettings => CurrentView is SettingsViewModel;
}

View File

@@ -21,12 +21,13 @@ public partial class TransactionFormViewModel : ViewModelBase
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
private bool _isEditMode = false;
public string FormTitle => IsEditMode ? "Edit Transaction" : "New Transaction";
public string FormTitle => IsEditMode ? (IsTransfer ? "Edit Transfer" : "Edit Transaction") : (IsTransfer ? "New Transfer" : "New Transaction");
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Transaction";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : (IsTransfer ? "Save Transfer" : "Save Transaction");
// ── Fields ──────────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsExpense), nameof(IsIncome), nameof(IsValid))]
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsExpense), nameof(IsIncome), nameof(IsTransfer), nameof(IsValid), nameof(FormTitle), nameof(SaveButtonLabel))]
private string _type = "expense";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
@@ -38,6 +39,7 @@ public partial class TransactionFormViewModel : ViewModelBase
[ObservableProperty] private string? _note;
[ObservableProperty] private List<DateTime> _dates = [DateTime.Now];
[ObservableProperty] private DateTime? _selectedDate;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(CurrencySymbol))]
private string _currency = "USD";
@@ -56,6 +58,9 @@ public partial class TransactionFormViewModel : ViewModelBase
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private Account? _selectedAccount;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private Account? _selectedToAccount;
[ObservableProperty] private ObservableCollection<Category> _categories = new();
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
@@ -66,24 +71,28 @@ public partial class TransactionFormViewModel : ViewModelBase
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public bool IsExpense => Type == "expense";
public bool IsIncome => Type == "income";
public bool IsTransfer => Type == "transfer";
public bool IsValid =>
decimal.TryParse(Amount, out var amt) && amt > 0 &&
!string.IsNullOrWhiteSpace(Description) &&
SelectedCategory is not null &&
SelectedAccount is not null &&
Dates is not null;
Dates is not null &&
(IsTransfer
? SelectedAccount is not null && SelectedToAccount is not null && SelectedAccount.Id != SelectedToAccount.Id
: !string.IsNullOrWhiteSpace(Description) && SelectedCategory is not null && SelectedAccount is not null);
// ── Callbacks ───────────────────────────────────────────
public Action? OnSaved;
public Action? OnCancelled;
public Action? OnDeleted;
public Action? OnOpenCategoryForm;
public Action<Category>? OnOpenEditCategoryForm;
[ObservableProperty] private bool _showDeleteConfirm = false;
// ── Edit mode: original transaction ─────────────────────
private Transaction? _editingTransaction;
private Guid? _editingId;
private Guid? _transferPairId;
private decimal _editingOriginalAmount;
private Guid? _editingOriginalCategoryId;
@@ -101,9 +110,10 @@ public partial class TransactionFormViewModel : ViewModelBase
public bool HasBudgetApproachingWarning => HasBudgetWarning && !BudgetWarningIsOverBudget;
// ── Commands ────────────────────────────────────────────
partial void OnSelectedCategoryChanged(Category? value)
{
if (value is null) return;
if (value.Type != Type) Type = value.Type;
CheckBudgetImpact();
}
@@ -120,6 +130,12 @@ public partial class TransactionFormViewModel : ViewModelBase
partial void OnTypeChanged(string value)
{
if (value == "transfer")
{
CheckBudgetImpact();
return;
}
if (value == SelectedCategory?.Type) return;
SelectedCategory = _categories.FirstOrDefault(c => c.Type == value);
CheckBudgetImpact();
@@ -127,6 +143,12 @@ public partial class TransactionFormViewModel : ViewModelBase
partial void OnSelectedAccountChanged(Account? value)
{
if (IsTransfer)
{
Currency = value?.Currency ?? "USD";
return;
}
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
var accountCurrency = value?.Currency ?? primaryCurrency;
Currency = accountCurrency;
@@ -136,6 +158,7 @@ public partial class TransactionFormViewModel : ViewModelBase
IsFetchingRate = true;
ExchangeRate = "";
}
ShowExchangeRateField = needsRate;
OnPropertyChanged(nameof(ExchangeRateLabel));
if (needsRate)
@@ -164,6 +187,7 @@ public partial class TransactionFormViewModel : ViewModelBase
BudgetWarningMessage = null;
BudgetWarningIsOverBudget = false;
if (IsTransfer) return;
if (Type != "expense") return;
if (SelectedCategory is null) return;
Debug.WriteLine(SelectedCategory.Name);
@@ -214,6 +238,16 @@ public partial class TransactionFormViewModel : ViewModelBase
}
}
[RelayCommand]
private void OpenCategoryForm() => OnOpenCategoryForm?.Invoke();
[RelayCommand]
private void OpenEditCategoryForm()
{
if (SelectedCategory is not null)
OnOpenEditCategoryForm?.Invoke(SelectedCategory);
}
[RelayCommand]
private void SetType(string type)
{
@@ -237,6 +271,37 @@ public partial class TransactionFormViewModel : ViewModelBase
return;
}
if (IsTransfer)
{
if (SelectedAccount is null || SelectedToAccount is null)
{
ErrorMessage = "Please select both accounts.";
return;
}
if (SelectedAccount.Id == SelectedToAccount.Id)
{
ErrorMessage = "From and To accounts must be different.";
return;
}
try
{
if (IsEditMode && _transferPairId.HasValue)
await DataRepo.General.UpdateTransfer(_transferPairId.Value, SelectedAccount.Id, SelectedToAccount.Id, amt, Dates.FirstOrDefault(), Note);
else
await DataRepo.General.InsertTransfer(SelectedAccount.Id, SelectedToAccount.Id, amt, Dates.FirstOrDefault(), Note);
OnSaved?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
DebugLogger.Log(ex);
}
return;
}
if (string.IsNullOrWhiteSpace(Description))
{
ErrorMessage = "Description is required.";
@@ -314,11 +379,14 @@ public partial class TransactionFormViewModel : ViewModelBase
[RelayCommand]
private async Task ConfirmDelete()
{
if (!IsEditMode || !_editingId.HasValue) return;
if (!IsEditMode) return;
try
{
await DataRepo.General.DeleteTransaction(_editingId.Value);
if (IsTransfer && _transferPairId.HasValue)
await DataRepo.General.DeleteTransfer(_transferPairId.Value);
else if (_editingId.HasValue)
await DataRepo.General.DeleteTransaction(_editingId.Value);
OnDeleted?.Invoke();
}
catch (Exception ex)
@@ -354,11 +422,12 @@ public partial class TransactionFormViewModel : ViewModelBase
ShowDeleteConfirm = false;
IsEditMode = false;
_editingId = null;
_transferPairId = null;
_editingOriginalAmount = 0;
_editingOriginalCategoryId = null;
Categories = AppData.Categories;
var sortedAccounts = new ObservableCollection<Account>(
AppData.Accounts.OrderByDescending(a => a.IsPrimary).ThenBy(a => a.CreatedAt));
AppData.Accounts.Where(a => !a.IsArchived).OrderByDescending(a => a.IsPrimary).ThenBy(a => a.CreatedAt));
Accounts = sortedAccounts;
Type = "expense";
Amount = "";
@@ -368,6 +437,7 @@ public partial class TransactionFormViewModel : ViewModelBase
ErrorMessage = null;
SelectedCategory = AppData.Categories.Count > 0 ? AppData.Categories[0] : null;
SelectedAccount = sortedAccounts.Count > 0 ? sortedAccounts[0] : null;
SelectedToAccount = sortedAccounts.Count > 1 ? sortedAccounts[1] : null;
ShowExchangeRateField = false;
ExchangeRate = "";
IsFetchingRate = false;
@@ -377,8 +447,7 @@ public partial class TransactionFormViewModel : ViewModelBase
}
/// <summary>Call this to open the form for editing an existing transaction.</summary>
public void SetupForEdit(
Transaction transaction)
public void SetupForEdit(Transaction transaction)
{
ShowDeleteConfirm = false;
IsEditMode = true;
@@ -386,30 +455,55 @@ public partial class TransactionFormViewModel : ViewModelBase
_editingOriginalAmount = transaction.Amount;
_editingOriginalCategoryId = transaction.CategoryId;
Categories = AppData.Categories;
Accounts = new ObservableCollection<Account>(
AppData.Accounts.OrderByDescending(a => a.IsPrimary).ThenBy(a => a.CreatedAt));
Type = transaction.Type;
var sortedAccounts = new ObservableCollection<Account>(
AppData.Accounts.Where(a => !a.IsArchived).OrderByDescending(a => a.IsPrimary).ThenBy(a => a.CreatedAt));
Accounts = sortedAccounts;
Amount = transaction.Amount.ToString("0.00");
Description = transaction.Description;
Note = transaction.Note;
Dates = [transaction.Date];
ErrorMessage = 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);
if (transaction.ExchangeRate.HasValue)
ResultTransaction = transaction;
if (transaction.IsTransfer && transaction.TransferPairId.HasValue)
{
ShowExchangeRateField = true;
ExchangeRate = transaction.ExchangeRate.Value.ToString("0.##########");
_transferPairId = transaction.TransferPairId;
Type = "transfer";
// Find the counterpart to determine from/to
var counterpart = AppData.Transactions.FirstOrDefault(t => t.TransferPairId == transaction.TransferPairId && t.Id != transaction.Id);
var outTx = transaction.IsTransferOut ? transaction : counterpart;
var inTx = transaction.IsTransferOut ? counterpart : transaction;
SelectedAccount = AppData.Accounts.FirstOrDefault(a => a.Id == outTx?.AccountId) ?? sortedAccounts.FirstOrDefault();
SelectedToAccount = AppData.Accounts.FirstOrDefault(a => a.Id == inTx?.AccountId) ?? sortedAccounts.Skip(1).FirstOrDefault();
Description = "Transfer";
SelectedCategory = null;
ShowExchangeRateField = false;
ExchangeRate = "";
IsFetchingRate = false;
}
else
{
ShowExchangeRateField = false;
ExchangeRate = "";
_transferPairId = null;
Type = transaction.Type;
Description = transaction.Description;
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)
?? (sortedAccounts.Count > 0 ? sortedAccounts[0] : null);
SelectedToAccount = sortedAccounts.Count > 1 ? sortedAccounts[1] : null;
if (transaction.ExchangeRate.HasValue)
{
ShowExchangeRateField = true;
ExchangeRate = transaction.ExchangeRate.Value.ToString("0.##########");
}
else
{
ShowExchangeRateField = false;
ExchangeRate = "";
}
IsFetchingRate = false;
}
IsFetchingRate = false;
ResultTransaction = transaction;
CheckBudgetImpact();
}
}

View File

@@ -23,7 +23,10 @@ public partial class TransactionsViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<Category> _categories = new();
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
[ObservableProperty] private List<Transaction> _filteredTransactions = new();
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilteredTransactionCount))]
private List<Transaction> _filteredTransactions = new();
public int FilteredTransactionCount => _filteredTransactions.Count;
private int _pageSize = 25;
[ObservableProperty] private int _pageSizeIndex;
@@ -84,7 +87,7 @@ public partial class TransactionsViewModel : ViewModelBase
new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month))
};
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilterTypeAll), nameof(FilterTypeIncome), nameof(FilterTypeExpense))]
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilterTypeAll), nameof(FilterTypeIncome), nameof(FilterTypeExpense), nameof(FilterTypeTransfer))]
private string _transactionType = "all";
@@ -157,8 +160,9 @@ public partial class TransactionsViewModel : ViewModelBase
private void ApplyFilters()
{
var filtered = AppData.Transactions.Where(x =>
x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| x.Note!.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
x.Type != "transfer_in" &&
(x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| x.Note!.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
var culture = new CultureInfo("en-US");
@@ -224,7 +228,7 @@ public partial class TransactionsViewModel : ViewModelBase
}
// Calculate totals based on date-filtered transactions (converted to primary currency)
// Calculate totals based on date-filtered transactions (transfers excluded)
TotalExpenses = filtered.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
TotalIncome = filtered.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
@@ -234,8 +238,12 @@ public partial class TransactionsViewModel : ViewModelBase
if (SelectedAccount.Name != "All Accounts")
filtered = filtered.Where(x => x.AccountId == SelectedAccount.Id);
if (TransactionType != "all")
filtered = filtered.Where(x => x.Type == TransactionType);
if (TransactionType == "income")
filtered = filtered.Where(x => x.Type == "income");
else if (TransactionType == "expense")
filtered = filtered.Where(x => x.Type == "expense");
else if (TransactionType == "transfer")
filtered = filtered.Where(x => x.IsTransfer);
switch (SelectedSortOption)
{
@@ -281,6 +289,7 @@ public partial class TransactionsViewModel : ViewModelBase
public bool FilterTypeAll => TransactionType == "all";
public bool FilterTypeIncome => TransactionType == "income";
public bool FilterTypeExpense => TransactionType == "expense";
public bool FilterTypeTransfer => TransactionType == "transfer";
[RelayCommand(CanExecute = nameof(HasNextPage))]
private void NextPage()
@@ -344,6 +353,7 @@ public partial class TransactionsViewModel : ViewModelBase
private void InitializeCategories()
{
Categories.Clear();
Categories.Insert(0, new Category() { Name = "All Categories" });
foreach (var appDataCategory in AppData.Categories)
{
@@ -355,6 +365,7 @@ public partial class TransactionsViewModel : ViewModelBase
private void InitializeAccounts()
{
Accounts.Clear();
Accounts.Insert(0, new Account() { Name = "All Accounts" });
foreach (var appDataAccount in AppData.Accounts)
{

View File

@@ -14,12 +14,20 @@
</Design.DataContext>
<Grid RowDefinitions="Auto,*" Margin="32,28,32,0">
<!-- TOP BAR -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,24">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,24">
<StackPanel Grid.Column="0">
<TextBlock Text="4 accounts" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding ActiveAccountCount, StringFormat='{}{0} 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" Command="{Binding CreateAccountCommand}">
<Button Grid.Column="1" Classes="base" Padding="14,9" VerticalAlignment="Center" Margin="0,0,10,0"
IsVisible="{Binding HasArchivedAccounts}"
Command="{Binding ShowArchivedListCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/archive.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
<TextBlock Text="Archived" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Grid.Column="2" 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; }" />
@@ -324,16 +332,26 @@
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Transaction">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" CornerRadius="8" Width="32" Height="32"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter='color'}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}" Width="14" Height="14"
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter='css'}" />
</Border>
<Panel Grid.Column="0" Width="32" Height="32" Margin="0,0,12,0">
<!-- Category icon -->
<Border CornerRadius="8" Width="32" Height="32"
IsVisible="{Binding !IsTransfer}">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter='color'}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}" Width="14" Height="14"
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter='css'}" />
</Border>
<!-- Transfer icon -->
<Border CornerRadius="8" Width="32" Height="32"
Background="{DynamicResource IconBgBlue}"
IsVisible="{Binding IsTransfer}">
<Svg Path="../Assets/Icons/arrow-right-left.svg" Width="14" Height="14"
Css="{DynamicResource SvgBlue}" />
</Border>
</Panel>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
<TextBlock Text="{Binding Description}" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
@@ -399,7 +417,9 @@
<TextBlock Text="Manage" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" Margin="0,0,0,4" />
<!-- Archive -->
<Button Background="Transparent" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="10" Padding="14,10"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Cursor="Hand">
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Cursor="Hand"
Command="{Binding RequestArchiveAccountCommand}"
CommandParameter="{Binding SelectedAccount}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Svg Path="../Assets/Icons/archive.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
<StackPanel Spacing="1">
@@ -431,8 +451,10 @@
</ScrollViewer>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="2">
<views:DeleteAccountDialogView IsVisible="{Binding DataContext.IsDeleteDialogVisible ,ElementName=AccountsPage }"
<views:DeleteAccountDialogView IsVisible="{Binding DataContext.IsDeleteDialogVisible, ElementName=AccountsPage}"
DataContext="{Binding Path=DeleteDialog}" />
<views:ArchiveAccountDialogView IsVisible="{Binding IsArchiveDialogVisible}" />
<views:ArchivedAccountsDialogView IsVisible="{Binding IsArchivedListVisible}" />
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,276 @@
<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:model="clr-namespace:Clario.Models"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
MinWidth="780" MinHeight="600"
x:DataType="vm:AnalyticsViewModel"
x:Class="Clario.Views.AnalyticsView">
<Design.DataContext>
<vm:AnalyticsViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*" Margin="32,28,32,0">
<!-- ── Top Bar ── -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,24">
<StackPanel Grid.Column="0">
<TextBlock Classes="muted" Text="Insights &amp; Trends" />
<TextBlock Text="Analytics" FontSize="26" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" Margin="0,2,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<!-- Period selector -->
<ComboBox ItemsSource="{Binding PeriodOptions}"
SelectedItem="{Binding SelectedPeriod}"
Background="{DynamicResource BgSurface}"
Foreground="{DynamicResource TextSecondary}"
BorderBrush="{DynamicResource BorderSubtle}"
CornerRadius="{DynamicResource RadiusControl}"
Padding="10,6" />
<!-- Export PDF button -->
<Button Classes="base" Padding="14,9"
Command="{Binding ExportPdfCommand}"
IsEnabled="{Binding !IsExporting}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/receipt-text.svg" Width="14" Height="14"
Css="{DynamicResource SvgSecondary}" />
<TextBlock Text="Export PDF"
VerticalAlignment="Center" FontSize="13" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
<!-- ── Scrollable Content ── -->
<ScrollViewer Grid.Row="1" Name="mainScrollviewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="20" Margin="0,0,0,32">
<!-- Export status message -->
<Border IsVisible="{Binding ExportStatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Background="{DynamicResource BadgeBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1" CornerRadius="10" Padding="12,8">
<TextBlock Text="{Binding ExportStatusMessage}"
Foreground="{DynamicResource AccentGreen}" FontSize="12" />
</Border>
<!-- ── Section 1: KPI Cards ── -->
<Grid ColumnDefinitions="*,*,*,*">
<Grid.Styles>
<Style Selector="Grid > Border">
<Setter Property="Margin" Value="0,0,16,0" />
</Style>
</Grid.Styles>
<!-- Total Income -->
<Border Grid.Column="0" Classes="card">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Background="{DynamicResource IconBgGreen}" CornerRadius="{DynamicResource RadiusIcon}" Padding="7">
<Svg Path="../Assets/Icons/trending-up.svg" Height="14" Width="14" Css="{DynamicResource SvgGreen}" />
</Border>
<TextBlock Classes="label" Text="TOTAL INCOME" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding TotalIncomeFormatted}" FontSize="20" FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" />
</StackPanel>
</Border>
<!-- Total Expenses -->
<Border Grid.Column="1" Classes="card">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Background="{DynamicResource IconBgRed}" CornerRadius="{DynamicResource RadiusIcon}" Padding="7">
<Svg Path="../Assets/Icons/trending-down.svg" Height="14" Width="14" Css="{DynamicResource SvgRed}" />
</Border>
<TextBlock Classes="label" Text="TOTAL EXPENSES" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding TotalExpensesFormatted}" FontSize="20" FontWeight="Bold"
Foreground="{DynamicResource AccentRed}" />
</StackPanel>
</Border>
<!-- Net Savings -->
<Border Grid.Column="2" Classes="card">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Background="{DynamicResource IconBgBlue}" CornerRadius="{DynamicResource RadiusIcon}" Padding="7">
<Svg Path="../Assets/Icons/piggy-bank.svg" Height="14" Width="14" Css="{DynamicResource SvgBlue}" />
</Border>
<TextBlock Classes="label" Text="NET SAVINGS" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding NetSavingsFormatted}" FontSize="20" FontWeight="Bold"
Foreground="{Binding NetSavingsPositive, Converter={StaticResource BoolToColorConverter}, ConverterParameter='#2ECC8A|#FF5E5E'}" />
</StackPanel>
</Border>
<!-- Savings Rate -->
<Border Grid.Column="3" Classes="card" Margin="0">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Background="{DynamicResource IconBgPurple}" CornerRadius="{DynamicResource RadiusIcon}" Padding="7">
<Svg Path="../Assets/Icons/percent.svg" Height="14" Width="14" Css="{DynamicResource SvgPurple}" />
</Border>
<TextBlock Classes="label" Text="SAVINGS RATE" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding SavingsRateFormatted}" FontSize="20" FontWeight="Bold"
Foreground="{DynamicResource AccentPurple}" />
</StackPanel>
</Border>
</Grid>
<!-- ── Section 2: Cash Flow Trend ── -->
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel>
<TextBlock Text="Cash Flow Trend" FontSize="{StaticResource FontSizeSectionHeading}"
FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Monthly income vs expenses" />
</StackPanel>
<lvc:CartesianChart Series="{Binding CashFlowSeries}"
XAxes="{Binding CashFlowXAxes}"
YAxes="{Binding CashFlowYAxes}"
Height="240"
Background="{DynamicResource BgSurface}"
LegendPosition="Bottom"
TooltipPosition="Top"
ZoomMode="None"
AnimationsSpeed="00:00:00.2" />
</StackPanel>
</Border>
<!-- ── Section 3: Net Worth ── -->
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel>
<TextBlock Text="Net Worth Progression" FontSize="{StaticResource FontSizeSectionHeading}"
FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Cumulative balance across all accounts" />
</StackPanel>
<lvc:CartesianChart Series="{Binding NetWorthSeries}"
XAxes="{Binding NetWorthXAxes}"
YAxes="{Binding NetWorthYAxes}"
Height="220"
Background="{DynamicResource BgSurface}"
LegendPosition="Hidden"
TooltipPosition="Top"
ZoomMode="None"
AnimationsSpeed="00:00:00.2" />
</StackPanel>
</Border>
<!-- ── Section 4+6: Day of Week + Income Sources ── -->
<Grid ColumnDefinitions="*,*" >
<!-- Day of Week -->
<Border Grid.Column="0" Classes="card" Margin="0,0,10,0">
<StackPanel Spacing="16">
<StackPanel>
<TextBlock Text="Spending by Day of Week" FontSize="{StaticResource FontSizeSectionHeading}"
FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Average daily spend per weekday" />
</StackPanel>
<lvc:CartesianChart Series="{Binding DayOfWeekSeries}"
XAxes="{Binding DayOfWeekXAxes}"
Height="200"
Background="{DynamicResource BgSurface}"
LegendPosition="Hidden"
TooltipPosition="Top"
ZoomMode="None"
AnimationsSpeed="00:00:00.2">
<lvc:CartesianChart.YAxes>
<lvc:XamlAxis IsVisible="False" />
</lvc:CartesianChart.YAxes>
</lvc:CartesianChart>
</StackPanel>
</Border>
<!-- Income Sources -->
<Border Grid.Column="1" Classes="card" Margin="10,0,0,0">
<StackPanel Spacing="16">
<StackPanel>
<TextBlock Text="Income Sources" FontSize="{StaticResource FontSizeSectionHeading}"
FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Breakdown by income category" />
</StackPanel>
<Panel Height="200">
<TextBlock Text="Not enough income data for this period."
Classes="muted" VerticalAlignment="Center" HorizontalAlignment="Center"
IsVisible="{Binding !HasIncomeSources}" />
<lvc:PieChart Series="{Binding IncomeSourcesSeries}"
IsVisible="{Binding HasIncomeSources}"
LegendPosition="Bottom"
TooltipPosition="Center"
AnimationsSpeed="00:00:00.2"
Background="{DynamicResource BgSurface}"/>
</Panel>
</StackPanel>
</Border>
</Grid>
<!-- ── Section 5: Top Categories ── -->
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel>
<TextBlock Text="Top Spending Categories" FontSize="{StaticResource FontSizeSectionHeading}"
FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Where your money is going" />
</StackPanel>
<TextBlock Text="No expense data for this period."
Classes="muted" IsVisible="{Binding !HasTopCategories}" />
<ItemsControl ItemsSource="{Binding TopCategories}"
IsVisible="{Binding HasTopCategories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:CategorySpendRow">
<Border Background="{DynamicResource BgHover}" CornerRadius="10" Padding="16,12" Margin="0,0,0,1">
<Grid ColumnDefinitions="Auto,*,Auto,80">
<!-- Icon -->
<Border Grid.Column="0" CornerRadius="8" Width="34" Height="34" Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
Width="16" Height="16"
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<!-- Name + progress bar -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<ProgressBar Value="{Binding Percentage}" Minimum="0" Maximum="100"
Height="4" Classes="blue" CornerRadius="2" />
</StackPanel>
<!-- Percentage -->
<TextBlock Grid.Column="2" Text="{Binding PercentageFormatted}"
Classes="muted" FontSize="12" VerticalAlignment="Center"
Margin="12,0" />
<!-- Amount -->
<TextBlock Grid.Column="3" Text="{Binding AmountFormatted}"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Right" VerticalAlignment="Center" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Clario.Views;
public partial class AnalyticsView : UserControl
{
public AnalyticsView()
{
InitializeComponent();
this.AddHandler(PointerWheelChangedEvent, WindowScrollHandler, RoutingStrategies.Tunnel);
}
private void WindowScrollHandler(object? sender, PointerWheelEventArgs e)
{
var offset = mainScrollviewer.Offset;
mainScrollviewer.Offset = new Vector(
offset.X,
offset.Y - e.Delta.Y * mainScrollviewer.SmallChange.Height * 3
);
e.Handled = true;
}
}

View File

@@ -0,0 +1,120 @@
<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.ArchiveAccountDialogView"
x:DataType="vm:AccountsViewModel"
x:CompileBindings="False">
<Design.DataContext>
<vm:AccountsViewModel />
</Design.DataContext>
<Grid>
<!-- Dim overlay -->
<Border Background="#70000000"/>
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentOrange}"
BorderThickness="1"
CornerRadius="18"
Padding="28"
Width="380"
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- Icon -->
<Border Background="{DynamicResource IconBgOrange}"
CornerRadius="14"
Width="54" Height="54"
HorizontalAlignment="Center"
Margin="0,0,0,16">
<Svg Path="../Assets/Icons/archive.svg"
Width="22" Height="22"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF7E5E; }"/>
</Border>
<!-- Title -->
<TextBlock Text="Archive 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 AccountToArchive.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15"/>
</Border.Background>
<Svg Path="{Binding AccountToArchive.Icon, Converter={StaticResource SvgPathFromName}}"
Width="13" Height="13"
Css="{Binding AccountToArchive.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
</Border>
<TextBlock Text="{Binding AccountToArchive.Name}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Description -->
<TextBlock Text="This account will be hidden from your active list and won't appear when adding transactions. You can restore it anytime."
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 CancelArchiveCommand}"/>
<Button Margin="6,0,0,0"
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="{DynamicResource AccentOrange}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmArchiveCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/archive.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }"/>
<TextBlock Text="Archive"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.Views;
public partial class ArchiveAccountDialogView : UserControl
{
public ArchiveAccountDialogView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,133 @@
<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:model="clr-namespace:Clario.Models"
mc:Ignorable="d"
x:Class="Clario.Views.ArchivedAccountsDialogView"
x:Name="ArchivedListView"
x:DataType="vm:AccountsViewModel"
x:CompileBindings="False">
<Design.DataContext>
<vm:AccountsViewModel />
</Design.DataContext>
<Grid>
<!-- Dim overlay -->
<Border Background="#70000000"/>
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="18"
Padding="28"
Width="440"
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- Header -->
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,18">
<StackPanel Grid.Column="0" Spacing="2">
<TextBlock Text="Archived Accounts"
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"/>
<TextBlock Text="Hidden from active lists and transaction forms"
FontSize="12"
Foreground="{DynamicResource TextMuted}"/>
</StackPanel>
<Button Grid.Column="1"
Background="Transparent"
BorderThickness="0"
Padding="6"
VerticalAlignment="Top"
Cursor="Hand"
Command="{Binding CloseArchivedListCommand}">
<Svg Path="../Assets/Icons/x.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}"/>
</Button>
</Grid>
<!-- Accounts list -->
<ScrollViewer MaxHeight="360" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ArchivedAccounts}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Account">
<Border Background="{DynamicResource BgBase}"
CornerRadius="10"
Padding="14,10">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Icon -->
<Border Grid.Column="0"
CornerRadius="10"
Width="40" Height="40"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15"/>
</Border.Background>
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
Width="18" Height="18"
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}"/>
</Border>
<!-- Name / meta -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="3">
<TextBlock Text="{Binding Name}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"/>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding Type}"
FontSize="11"
Foreground="{DynamicResource TextMuted}"/>
<TextBlock Text="·"
FontSize="11"
Foreground="{DynamicResource TextDisabled}"/>
<TextBlock Text="{Binding Currency}"
FontSize="11"
Foreground="{DynamicResource TextMuted}"/>
</StackPanel>
</StackPanel>
<!-- Restore button -->
<Button Grid.Column="2"
Background="Transparent"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="8"
Padding="10,6"
Cursor="Hand"
Command="{Binding DataContext.UnarchiveAccountCommand, ElementName=ArchivedListView}"
CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/rotate-ccw.svg"
Width="12" Height="12"
Css="{DynamicResource SvgBlue}"/>
<TextBlock Text="Restore"
FontSize="11"
FontWeight="SemiBold"
Foreground="{DynamicResource AccentBlue}"/>
</StackPanel>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.Views;
public partial class ArchivedAccountsDialogView : UserControl
{
public ArchivedAccountsDialogView()
{
InitializeComponent();
}
}

View File

@@ -191,19 +191,18 @@
</Button>
<!-- Error message -->
<!-- REPLACE: IsVisible="{Binding HasError}" Text="{Binding ErrorMessage}" -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,10"
Margin="0,0,0,16"
IsVisible="False">
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="Invalid email or password."
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center" />
@@ -392,19 +391,18 @@
</Border>
<!-- Error message -->
<!-- REPLACE: IsVisible="{Binding HasError}" Text="{Binding ErrorMessage}" -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,10"
Margin="0,0,0,16"
IsVisible="False">
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="Something went wrong. Please try again."
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center" />

View File

@@ -267,7 +267,7 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Budgeted" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<!-- REPLACE: bind to TotalBudgetedFormatted -->
<TextBlock FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock Grid.Column="1" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
@@ -279,7 +279,7 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Spent" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<!-- REPLACE: bind to TotalSpentFormatted -->
<TextBlock FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock Grid.Column="1" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />

View File

@@ -0,0 +1,325 @@
<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.CategoryFormView"
x:DataType="vm:CategoryFormViewModel"
x:Name="CategoryFormRoot">
<Design.DataContext>
<vm:CategoryFormViewModel />
</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="480"
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. Groceries"
FontSize="13"
Height="38"
Padding="12,0"
VerticalContentAlignment="Center"
Margin="0,0,0,16" />
<!-- ── Type toggle ─────────────────── -->
<TextBlock Text="TYPE" 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,16">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0"
Classes="nav"
Classes.accented="{Binding IsExpense}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="expense">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-up-right.svg" Width="13" Height="13" />
<TextBlock Text="Expense" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Grid.Column="1"
Classes="nav"
Classes.accented="{Binding IsIncome}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="income">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-down-left.svg" Width="13" Height="13" />
<TextBlock Text="Income" FontSize="13" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
<!-- ── Icon picker ─────────────────── -->
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="8"
Margin="0,0,0,16">
<ScrollViewer MaxHeight="148"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding CategoryIcons}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Classes="nav"
Width="44"
Height="44"
Padding="0"
Margin="2"
CornerRadius="8"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding DataContext.SetIconCommand, ElementName=CategoryFormRoot}"
CommandParameter="{Binding}">
<Classes.accented>
<MultiBinding Converter="{StaticResource EqualValueConverter}">
<Binding Path="." />
<Binding Path="DataContext.SelectedIcon" ElementName="CategoryFormRoot" />
</MultiBinding>
</Classes.accented>
<Svg Path="{Binding Converter={StaticResource SvgPathFromName}}"
Width="16" Height="16"
/>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<!-- ── Color ──────────────────────── -->
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="8"
Margin="0,0,0,20">
<ColorPicker
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Width="400"
Height="40"
CornerRadius="{DynamicResource RadiusControl}"
IsAlphaEnabled="False"
IsAlphaVisible="False"
IsColorPaletteVisible="False"
IsAccentColorsVisible="False" />
</Border>
<!-- ── 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>
<!-- ── Delete button (edit mode only) ── -->
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,10"
Margin="0,0,0,10"
IsVisible="{Binding IsEditMode}"
IsEnabled="{Binding CanDelete}"
Command="{Binding RequestDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="{Binding CanDelete, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Delete Category|Min. 4 categories required'}"
FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
<!-- ── 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">
<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>
<TextBlock Text="Delete Category"
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,8" />
<TextBlock Text="This action cannot be undone. The category will be permanently removed."
FontSize="13"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,24" />
<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>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.Views;
public partial class CategoryFormView : UserControl
{
public CategoryFormView()
{
InitializeComponent();
}
}

View File

@@ -25,14 +25,6 @@
Foreground="{DynamicResource TextPrimary}" Margin="0,2,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<Border Background="{DynamicResource BgSurface}" CornerRadius="{StaticResource RadiusControl}"
BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" Padding="14,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/calendar-days.svg" Height="13" Width="13" />
<TextBlock Text="Jan Mar 2026" FontSize="{StaticResource FontSizeBody}" Foreground="{DynamicResource TextSecondary}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<Button Background="{DynamicResource AccentBlue}" Foreground="{DynamicResource BgBase}" FontWeight="SemiBold"
FontSize="{StaticResource FontSizeBody}" CornerRadius="{StaticResource RadiusControl}" Padding="16,8" BorderThickness="0"
Cursor="Hand" Content="+ Add Transaction" Command="{Binding CreateTransactionCommand}" />
@@ -384,7 +376,8 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Balance" FontSize="{StaticResource FontSizeBody}" FontWeight="SemiBold"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Grid.Column="1" FontSize="{StaticResource FontSizeSectionHeading}" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
<TextBlock Grid.Column="1" FontSize="{StaticResource FontSizeSectionHeading}" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />

View File

@@ -86,7 +86,9 @@
</StackPanel>
</Button>
<TextBlock Classes="label" Text="REPORTS" Margin="12,20,0,10" />
<Button Classes="nav" HorizontalAlignment="Stretch">
<Button Classes="nav" HorizontalAlignment="Stretch"
Classes.active="{Binding isOnAnalytics}"
Command="{Binding GoToAnalyticsCommand}">
<StackPanel Orientation="Horizontal" Spacing="12">
<Svg Path="../Assets/Icons/chart-no-axes-combined.svg" Height="14" Width="14" />
<TextBlock Text="Analytics" FontSize="{StaticResource FontSizeBody}" VerticalAlignment="Center" />
@@ -117,6 +119,9 @@
<views:SetSavingsGoalDialogView
DataContext="{Binding SetSavingsGoalDialogViewModel}"
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
<views:CategoryFormView
DataContext="{Binding CategoryFormViewModel}"
IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" />
</Grid>
</Grid>

View File

@@ -74,7 +74,7 @@
CornerRadius="{DynamicResource RadiusControl}"
Padding="3"
Margin="0,0,0,20">
<Grid ColumnDefinitions="*,*">
<Grid ColumnDefinitions="*,*,*">
<!-- Expense -->
<Button Grid.Column="0"
Classes="nav"
@@ -114,6 +114,25 @@
VerticalAlignment="Center" />
</StackPanel>
</Button>
<!-- Transfer -->
<Button Grid.Column="2"
Classes="nav"
Classes.accented="{Binding IsTransfer}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="7"
Padding="0,8"
Focusable="False"
Command="{Binding SetTypeCommand}"
CommandParameter="transfer">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-right-left.svg"
Width="13" Height="13" />
<TextBlock Text="Transfer"
FontSize="13"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
@@ -155,25 +174,28 @@
</Grid>
</Border>
<!-- ── Description ─────────────────── -->
<TextBlock Text="DESCRIPTION" Classes="label" Margin="0,0,0,6" />
<!-- ── Description (hidden for transfers) ─── -->
<TextBlock Text="DESCRIPTION" Classes="label" Margin="0,0,0,6"
IsVisible="{Binding !IsTransfer}" />
<TextBox Text="{Binding Description, Mode=TwoWay}"
Watermark="e.g. Grocery Shopping"
FontSize="13"
Height="38"
Padding="12,0"
VerticalContentAlignment="Center"
Margin="0,0,0,16" />
Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}" />
<!-- ── Category + Account ──────────── -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- ── Category + Account (income/expense) ── -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}">
<!-- Category -->
<StackPanel Grid.Column="0" Spacing="6">
<StackPanel Orientation="Horizontal">
<TextBlock Text="CATEGORY" Classes="label" />
<Button Classes="nav" Padding="2 0">
<Svg Path="../Assets/Icons/plus.svg" Height="11" Width="11"></Svg>
<Button Classes="nav" Padding="2,0" Command="{Binding OpenCategoryFormCommand}">
<Svg Path="../Assets/Icons/plus.svg" Height="11" Width="11" />
</Button>
</StackPanel>
<Border Background="{DynamicResource BgBase}"
@@ -182,6 +204,7 @@
CornerRadius="{DynamicResource RadiusControl}">
<Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0"
Classes="editable"
CornerRadius="7"
Width="30" Height="30"
Margin="8,0,0,0"
@@ -191,9 +214,23 @@
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}" />
<Panel>
<Svg Classes="hide"
Path="{Binding SelectedCategory.Icon, Converter={StaticResource SvgPathFromName}}"
Width="14" Height="14"
Css="{Binding SelectedCategory.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
<Button Classes="base reveal"
Width="30" Height="30"
Margin="0"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding OpenEditCategoryFormCommand}">
<Svg Path="../Assets/Icons/pencil.svg"
Width="12" Height="12"
Css="{DynamicResource SvgSecondary}" />
</Button>
</Panel>
</Border>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Categories}"
@@ -245,6 +282,90 @@
</Grid>
<!-- ── From + To accounts (transfer) ── -->
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
IsVisible="{Binding IsTransfer}">
<!-- From Account -->
<StackPanel Grid.Column="0" Spacing="6">
<TextBlock Text="FROM ACCOUNT" 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 SelectedAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding SelectedAccount.Icon, Converter={StaticResource SvgPathFromName}}"
Width="14" Height="14"
Css="{Binding SelectedAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Accounts}"
SelectedItem="{Binding SelectedAccount, Mode=TwoWay}"
DisplayMemberBinding="{Binding Name}"
Background="Transparent"
BorderThickness="0"
Padding="8,10"
FontSize="13"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
</StackPanel>
<!-- Arrow -->
<Svg Grid.Column="1"
Path="../Assets/Icons/arrow-right.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Bottom"
Margin="8,0,8,10" />
<!-- To Account -->
<StackPanel Grid.Column="2" Spacing="6">
<TextBlock Text="TO ACCOUNT" 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 SelectedToAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding SelectedToAccount.Icon, Converter={StaticResource SvgPathFromName}}"
Width="14" Height="14"
Css="{Binding SelectedToAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Accounts}"
SelectedItem="{Binding SelectedToAccount, Mode=TwoWay}"
DisplayMemberBinding="{Binding Name}"
Background="Transparent"
BorderThickness="0"
Padding="8,10"
FontSize="13"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
</StackPanel>
</Grid>
<!-- ── Exchange Rate (shown for foreign-currency accounts) ── -->
<Border IsVisible="{Binding ShowExchangeRateField}"
Background="{DynamicResource BgBase}"

View File

@@ -22,10 +22,11 @@
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,1,0"
Padding="20,28,20,28">
Padding="4,0,4,0">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Spacing="0">
HorizontalScrollBarVisibility="Disabled"
Padding="16,0,16,0">
<StackPanel Spacing="0" Margin="0 28 0 28">
<!-- Period header ─
REPLACE: bind TextBlock texts to SelectedPeriodLabel
@@ -210,8 +211,7 @@
CornerRadius="{DynamicResource RadiusControl}"
Padding="3"
Margin="0,0,0,14">
<Grid ColumnDefinitions="*,*,*">
<!-- Active pill -->
<Grid ColumnDefinitions="*,*,*,*">
<Button Grid.Column="0"
Classes="nav"
Classes.accented="{Binding FilterTypeAll}"
@@ -222,10 +222,7 @@
Padding="0,6"
Command="{Binding SetTransactionTypeCommand}"
CommandParameter="all">
<TextBlock Text="All"
FontSize="12"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock Text="All" FontSize="12" FontWeight="SemiBold" HorizontalAlignment="Center" />
</Button>
<Button Grid.Column="1"
Classes="nav"
@@ -237,9 +234,7 @@
Padding="0,6"
Command="{Binding SetTransactionTypeCommand}"
CommandParameter="income">
<TextBlock Text="Income"
FontSize="12" Focusable="False"
HorizontalAlignment="Center" />
<TextBlock Text="Income" FontSize="12" Focusable="False" HorizontalAlignment="Center" />
</Button>
<Button Grid.Column="2"
Classes="nav"
@@ -251,9 +246,19 @@
Padding="0,6"
Command="{Binding SetTransactionTypeCommand}"
CommandParameter="expense">
<TextBlock Text="Expense"
FontSize="12" Focusable="False"
HorizontalAlignment="Center" />
<TextBlock Text="Expense" FontSize="12" Focusable="False" HorizontalAlignment="Center" />
</Button>
<Button Grid.Column="3"
Classes="nav"
Classes.accented="{Binding FilterTypeTransfer}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Focusable="False"
CornerRadius="7"
Padding="0,6"
Command="{Binding SetTransactionTypeCommand}"
CommandParameter="transfer">
<TextBlock Text="Transfer" FontSize="12" Focusable="False" HorizontalAlignment="Center" />
</Button>
</Grid>
</Border>
@@ -324,8 +329,7 @@
ColumnDefinitions="*,Auto"
Margin="28,28,28,0">
<StackPanel Grid.Column="0">
<!-- REPLACE: bind to FilteredTransactionCount -->
<TextBlock Text="46 transactions"
<TextBlock Text="{Binding FilteredTransactionCount, StringFormat='{}{0} transactions'}"
Classes="muted" />
<TextBlock Text="Transactions"
FontSize="26"
@@ -424,30 +428,42 @@
BorderThickness="0,1,0,0">
<Grid ColumnDefinitions="44,*,160,120,100,100">
<Border Grid.Column="0"
CornerRadius="{DynamicResource RadiusIcon}"
Width="34" Height="34"
VerticalAlignment="Center" Padding="0">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Panel>
<Svg Classes="hide" Path="{Binding Category.Icon,Converter={StaticResource SvgPathFromName}}"
Width="16"
Height="16"
<Panel Grid.Column="0"
Width="34" Height="34"
VerticalAlignment="Center">
<!-- Category icon (income / expense) -->
<Border CornerRadius="{DynamicResource RadiusIcon}"
Width="34" Height="34"
IsVisible="{Binding !IsTransfer}">
<Border.Background>
<SolidColorBrush
Color="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Classes="hide"
Path="{Binding Category.Icon,Converter={StaticResource SvgPathFromName}}"
Width="16" Height="16"
Css="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
<Button Classes="base reveal" CornerRadius="{DynamicResource RadiusSmall}" Width="34"
Height="34" Margin="0"
Command="{Binding DataContext.EditTransactionCommand,ElementName=transactionsControl}"
CommandParameter="{Binding .}">
<Svg Path="../Assets/Icons/pencil.svg" Width="16" Height="16"
Css="{DynamicResource SvgSecondary}" />
</Button>
</Panel>
</Border>
</Border>
<!-- Transfer icon -->
<Border CornerRadius="{DynamicResource RadiusIcon}"
Width="34" Height="34"
Background="{DynamicResource IconBgBlue}"
IsVisible="{Binding IsTransfer}">
<Svg Path="../Assets/Icons/arrow-right-left.svg"
Width="16" Height="16"
Css="{DynamicResource SvgBlue}" />
</Border>
<!-- Edit button overlay -->
<Button Classes="base reveal"
CornerRadius="{DynamicResource RadiusSmall}"
Width="34" Height="34" Margin="0"
Command="{Binding DataContext.EditTransactionCommand,ElementName=transactionsControl}"
CommandParameter="{Binding .}">
<Svg Path="../Assets/Icons/pencil.svg" Width="16" Height="16"
Css="{DynamicResource SvgSecondary}" />
</Button>
</Panel>
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
@@ -463,24 +479,21 @@
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
<!-- REPLACE: bind to Transaction.Category -->
<Border Grid.Column="2"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,0">
<StackPanel Orientation="Horizontal" Spacing="6">
<Border
CornerRadius="6"
Padding="6,3">
<Panel Grid.Column="2"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<!-- Normal category badge -->
<StackPanel Orientation="Horizontal" Spacing="6"
IsVisible="{Binding !IsTransfer}">
<Border CornerRadius="6" Padding="6,3">
<Border.Background>
<SolidColorBrush Opacity="0.15"
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}">
</SolidColorBrush>
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}" />
</Border.Background>
<StackPanel Orientation="Horizontal" Spacing="5">
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
Width="11" Height="11"
Css="{Binding Category.Color,Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
<TextBlock Text="{Binding Category.Name}"
FontSize="12"
Foreground="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=brush}"
@@ -488,16 +501,28 @@
</StackPanel>
</Border>
</StackPanel>
</Border>
<!-- Transfer badge -->
<Border CornerRadius="6" Padding="6,3"
Background="{DynamicResource IconBgBlue}"
IsVisible="{Binding IsTransfer}">
<StackPanel Orientation="Horizontal" Spacing="5">
<Svg Path="../Assets/Icons/arrow-right-left.svg"
Width="11" Height="11"
Css="{DynamicResource SvgBlue}" />
<TextBlock Text="Transfer"
FontSize="12"
Foreground="{DynamicResource AccentBlue}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
</Panel>
<!-- REPLACE: bind to Transaction.Account -->
<TextBlock Grid.Column="3"
Text="{Binding AccountId, Converter={StaticResource AccountFromIdConverter}}"
<TextBlock Grid.Column="3"
Text="{Binding AccountDisplayText}"
FontSize="12"
Foreground="{DynamicResource TextMuted}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,0" />
HorizontalAlignment="Center" />
<!-- REPLACE: bind to Transaction.DateFormatted -->
<TextBlock Grid.Column="4"

View File

@@ -20,6 +20,7 @@
<PackageVersion Include="Deadpikle.AvaloniaProgressRing" Version="0.10.10" />
<PackageVersion Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
<PackageVersion Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc6.1" />
<PackageVersion Include="QuestPDF" Version="2026.2.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.116.1" />
<PackageVersion Include="Supabase" Version="1.1.1" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.15" />