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
@@ -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" />
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
16
Clario/Assets/Icons/bike.svg
Normal 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 |
14
Clario/Assets/Icons/book-open.svg
Normal 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 |
19
Clario/Assets/Icons/bus.svg
Normal 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 |
14
Clario/Assets/Icons/camera.svg
Normal 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 |
16
Clario/Assets/Icons/coffee.svg
Normal 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 |
17
Clario/Assets/Icons/dumbbell.svg
Normal 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 |
20
Clario/Assets/Icons/film.svg
Normal 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 |
16
Clario/Assets/Icons/gift.svg
Normal 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 |
15
Clario/Assets/Icons/graduation-cap.svg
Normal 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 |
13
Clario/Assets/Icons/headphones.svg
Normal 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 |
14
Clario/Assets/Icons/leaf.svg
Normal 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 |
15
Clario/Assets/Icons/music.svg
Normal 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 |
16
Clario/Assets/Icons/package.svg
Normal 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 |
17
Clario/Assets/Icons/pizza.svg
Normal 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 |
13
Clario/Assets/Icons/plane.svg
Normal 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 |
17
Clario/Assets/Icons/scissors.svg
Normal 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 |
13
Clario/Assets/Icons/shirt.svg
Normal 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 |
14
Clario/Assets/Icons/smartphone.svg
Normal 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 |
17
Clario/Assets/Icons/stethoscope.svg
Normal 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 |
18
Clario/Assets/Icons/train-front.svg
Normal 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 |
14
Clario/Assets/Icons/tv.svg
Normal 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 |
16
Clario/Assets/Icons/wine.svg
Normal 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 |
13
Clario/Assets/Icons/wrench.svg
Normal 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 |
@@ -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,16 +23,17 @@
|
||||
<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>
|
||||
@@ -43,9 +44,15 @@
|
||||
<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>
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<None Update="devsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
||||
@@ -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
@@ -0,0 +1,14 @@
|
||||
namespace Clario.Enums;
|
||||
|
||||
public enum AuthError
|
||||
{
|
||||
InvalidCredentials,
|
||||
EmailAlreadyExists,
|
||||
EmailNotConfirmed,
|
||||
WeakPassword,
|
||||
InvalidEmail,
|
||||
SignupDisabled,
|
||||
RateLimited,
|
||||
SessionExpired,
|
||||
Unknown
|
||||
}
|
||||
408
Clario/MobileViews/AccountFormViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/AccountFormViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AccountFormViewMobile : UserControl
|
||||
{
|
||||
public AccountFormViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Classes="base"
|
||||
Padding="12,8"
|
||||
VerticalAlignment="Center">
|
||||
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">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
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}"
|
||||
<!-- 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"
|
||||
VerticalAlignment="Top"
|
||||
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>
|
||||
@@ -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);
|
||||
|
||||
161
Clario/MobileViews/AnalyticsViewMobile.axaml
Normal 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 & 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>
|
||||
11
Clario/MobileViews/AnalyticsViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AnalyticsViewMobile : UserControl
|
||||
{
|
||||
public AnalyticsViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
123
Clario/MobileViews/ArchiveAccountDialogViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/ArchiveAccountDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class ArchiveAccountDialogViewMobile : UserControl
|
||||
{
|
||||
public ArchiveAccountDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
138
Clario/MobileViews/ArchivedAccountsDialogViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/ArchivedAccountsDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class ArchivedAccountsDialogViewMobile : UserControl
|
||||
{
|
||||
public ArchivedAccountsDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
387
Clario/MobileViews/AuthViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/AuthViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AuthViewMobile : UserControl
|
||||
{
|
||||
public AuthViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
393
Clario/MobileViews/BudgetFormViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/BudgetFormViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class BudgetFormViewMobile : UserControl
|
||||
{
|
||||
public BudgetFormViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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}" />
|
||||
|
||||
320
Clario/MobileViews/CategoryFormViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/CategoryFormViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class CategoryFormViewMobile : UserControl
|
||||
{
|
||||
public CategoryFormViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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,28 +174,30 @@
|
||||
<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>
|
||||
|
||||
<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">
|
||||
TooltipPosition="Hidden"
|
||||
ZoomMode="None"
|
||||
AnimationsSpeed="00:00:00.1">
|
||||
<lvc:CartesianChart.XAxes>
|
||||
<lvc:XamlAxis IsVisible="False" />
|
||||
</lvc:CartesianChart.XAxes>
|
||||
@@ -212,13 +218,13 @@
|
||||
<TextBlock Text="{Binding Name}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border HorizontalAlignment="Stretch"
|
||||
Height="1"
|
||||
<Border HorizontalAlignment="Stretch" Height="1"
|
||||
Background="{DynamicResource BorderSubtle}" />
|
||||
|
||||
<!-- Category amounts -->
|
||||
@@ -230,15 +236,38 @@
|
||||
</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}}" />
|
||||
<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,24 +281,22 @@
|
||||
<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>
|
||||
|
||||
<Panel>
|
||||
<!-- Transaction rows -->
|
||||
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
|
||||
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10"
|
||||
IsVisible="{Binding HasTransactionData}">
|
||||
<ItemsControl ItemsSource="{Binding RecentTransactions}"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
CornerRadius="10">
|
||||
@@ -284,8 +311,7 @@
|
||||
Background="{DynamicResource BgSurface}"
|
||||
Margin="12,10">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="9"
|
||||
Width="36" Height="36"
|
||||
CornerRadius="9" Width="36" Height="36"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
@@ -298,8 +324,7 @@
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Text="{Binding Category.Name}" Classes="muted" FontSize="11" />
|
||||
@@ -310,8 +335,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Text>
|
||||
@@ -327,6 +351,22 @@
|
||||
</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,12 +379,14 @@
|
||||
<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}">
|
||||
|
||||
<Panel>
|
||||
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}"
|
||||
IsVisible="{Binding HasBudgetData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="14" />
|
||||
@@ -360,36 +402,52 @@
|
||||
Css="{DynamicResource SvgSecondary}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding Category.Name}"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center"
|
||||
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="{Binding SpentFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}" FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding LimitFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsOverBudget}">
|
||||
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text="{Binding SpentFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}" 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" />
|
||||
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>
|
||||
|
||||
|
||||
454
Clario/MobileViews/DeleteAccountDialogViewMobile.axaml
Normal 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 & 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 & Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/DeleteAccountDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class DeleteAccountDialogViewMobile : UserControl
|
||||
{
|
||||
public DeleteAccountDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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}" />
|
||||
|
||||
150
Clario/MobileViews/SetSavingsGoalDialogViewMobile.axaml
Normal 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>
|
||||
11
Clario/MobileViews/SetSavingsGoalDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class SetSavingsGoalDialogViewMobile : UserControl
|
||||
{
|
||||
public SetSavingsGoalDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
486
Clario/MobileViews/SettingsViewMobile.axaml
Normal 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 & 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>
|
||||
11
Clario/MobileViews/SettingsViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class SettingsViewMobile : UserControl
|
||||
{
|
||||
public SettingsViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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,19 +294,30 @@
|
||||
Classes="label" />
|
||||
</Border>
|
||||
|
||||
<!-- Transaction row -->
|
||||
<!-- 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"
|
||||
IsVisible="{Binding !GroupHeader}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="{DynamicResource RadiusIcon}"
|
||||
<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}"
|
||||
@@ -304,6 +327,16 @@
|
||||
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"
|
||||
@@ -314,9 +347,16 @@
|
||||
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}" />
|
||||
@@ -345,6 +385,7 @@
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Button>
|
||||
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
|
||||
12
Clario/Models/CategorySpendRow.cs
Normal 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}%";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
300
Clario/Services/PdfExportService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
436
Clario/ViewModels/AnalyticsViewModel.cs
Normal 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 Mon–Sun (index 0–6 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
192
Clario/ViewModels/CategoryFormViewModel.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -104,6 +113,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
|
||||
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,10 +379,13 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private async Task ConfirmDelete()
|
||||
{
|
||||
if (!IsEditMode || !_editingId.HasValue) return;
|
||||
if (!IsEditMode) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (IsTransfer && _transferPairId.HasValue)
|
||||
await DataRepo.General.DeleteTransfer(_transferPairId.Value);
|
||||
else if (_editingId.HasValue)
|
||||
await DataRepo.General.DeleteTransaction(_editingId.Value);
|
||||
OnDeleted?.Invoke();
|
||||
}
|
||||
@@ -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,18 +455,41 @@ 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;
|
||||
ResultTransaction = transaction;
|
||||
|
||||
if (transaction.IsTransfer && transaction.TransferPairId.HasValue)
|
||||
{
|
||||
_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
|
||||
{
|
||||
_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)
|
||||
?? (AppData.Accounts.Count > 0 ? AppData.Accounts[0] : null);
|
||||
?? (sortedAccounts.Count > 0 ? sortedAccounts[0] : null);
|
||||
SelectedToAccount = sortedAccounts.Count > 1 ? sortedAccounts[1] : null;
|
||||
if (transaction.ExchangeRate.HasValue)
|
||||
{
|
||||
ShowExchangeRateField = true;
|
||||
@@ -408,8 +500,10 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
ShowExchangeRateField = false;
|
||||
ExchangeRate = "";
|
||||
}
|
||||
|
||||
IsFetchingRate = false;
|
||||
ResultTransaction = transaction;
|
||||
}
|
||||
|
||||
CheckBudgetImpact();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,8 +332,10 @@
|
||||
<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">
|
||||
<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'}"
|
||||
@@ -334,6 +344,14 @@
|
||||
<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>
|
||||
276
Clario/Views/AnalyticsView.axaml
Normal 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 & 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>
|
||||
26
Clario/Views/AnalyticsView.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
120
Clario/Views/ArchiveAccountDialogView.axaml
Normal 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>
|
||||
11
Clario/Views/ArchiveAccountDialogView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class ArchiveAccountDialogView : UserControl
|
||||
{
|
||||
public ArchiveAccountDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
133
Clario/Views/ArchivedAccountsDialogView.axaml
Normal 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>
|
||||
11
Clario/Views/ArchivedAccountsDialogView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class ArchivedAccountsDialogView : UserControl
|
||||
{
|
||||
public ArchivedAccountsDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
325
Clario/Views/CategoryFormView.axaml
Normal 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>
|
||||
11
Clario/Views/CategoryFormView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class CategoryFormView : UserControl
|
||||
{
|
||||
public CategoryFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}"
|
||||
<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}"
|
||||
|
||||
@@ -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,22 +428,36 @@
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid ColumnDefinitions="44,*,160,120,100,100">
|
||||
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="{DynamicResource RadiusIcon}"
|
||||
<Panel Grid.Column="0"
|
||||
Width="34" Height="34"
|
||||
VerticalAlignment="Center" Padding="0">
|
||||
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>
|
||||
<Panel>
|
||||
<Svg Classes="hide" Path="{Binding Category.Icon,Converter={StaticResource SvgPathFromName}}"
|
||||
Width="16"
|
||||
Height="16"
|
||||
<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"
|
||||
</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"
|
||||
@@ -447,8 +465,6 @@
|
||||
</Button>
|
||||
</Panel>
|
||||
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,16,0">
|
||||
@@ -463,24 +479,21 @@
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- REPLACE: bind to Transaction.Category -->
|
||||
<Border Grid.Column="2"
|
||||
<Panel Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Border
|
||||
CornerRadius="6"
|
||||
Padding="6,3">
|
||||
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>
|
||||
<!-- 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}}"
|
||||
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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||