Files
PolyTraderSharp/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml.cs
T
RichardandClaude Opus 5 dd8da3fd3f Arbeitsstand gesichert: Deploymentcenter-Integration (D-0 bis D-5) und Einfenster-Shell
Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt,
welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen.

Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json
ueberschneiden und darum gemeinsam abgelegt werden:

Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig):
- Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert
- DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli
- WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt
  (version, os, checks, metrics, status stopped)
- Security: MasterKeyResolver, SecretRedactor, FilePermissions
- Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin)
- deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben
  ueber .gitignore aussen vor
- setup.json fuer die Erstinstallation
- UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md
  verweist auf den neuen Plan

Einfenster-Shell (UI-Redesign):
- ShellWindow + ShellNavModel als Seitenleisten-Shell
- WindowMenuBar und WindowMenuModel entfallen
- Modul- und Kernfenster auf die Shell-Einbettung angepasst

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:42:22 +02:00

257 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.Accounting.Logic;
using PolyTrader.Modules.Accounting.Models;
using PolyTrader.Modules.Accounting.Persistence;
using PolyTrader.Modules.Accounting.Services;
using PolyTraderSharp;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views.Modules
{
/// <summary>
/// Accounting: Periodenabrechnung mit KPI-Kacheln und Monatsübersicht, Ledger-Ansicht und
/// Abruf-/Status-Tab. Layout vollständig in AccountingWindow.axaml.
/// </summary>
public partial class AccountingWindow : UserControl
{
private readonly ObservableCollection<MonthlyRow> _monthly = new();
private readonly ObservableCollection<LedgerEntry> _ledger = new();
private readonly ObservableCollection<IngestRun> _runs = new();
private readonly TradingState _state = null!;
private readonly AccountingReportService _reports = null!;
private readonly ILedgerRepository _ledgerRepo = null!;
private readonly IIngestRunRepository _runRepo = null!;
private readonly AccountingIngestService _ingest = null!;
private readonly TerminalLogger _logger = null!;
private PeriodStatement? _statement;
private CurrencyContext _currency = new("USDC", 1m, true, string.Empty);
public AccountingWindow() => AvaloniaXamlLoader.Load(this);
public AccountingWindow(IModuleUiHost host, TradingState state, AccountingReportService reports,
ILedgerRepository ledgerRepo, IIngestRunRepository runRepo,
AccountingIngestService ingest, TerminalLogger logger) : this()
{
_state = state;
_reports = reports;
_ledgerRepo = ledgerRepo;
_runRepo = runRepo;
_ingest = ingest;
_logger = logger;
this.FindControl<DataGrid>("gridMonthly")!.ItemsSource = _monthly;
this.FindControl<DataGrid>("gridLedger")!.ItemsSource = _ledger;
this.FindControl<DataGrid>("gridRuns")!.ItemsSource = _runs;
var cbCurrency = this.FindControl<ComboBox>("cbCurrency")!;
cbCurrency.ItemsSource = new[] { "USDC", "USD", "EUR" };
cbCurrency.SelectedIndex = 0;
// Vorbelegung wie bisher: laufender Monat.
var now = DateTime.Now;
this.FindControl<CalendarDatePicker>("dtFrom")!.SelectedDate = new DateTime(now.Year, now.Month, 1);
this.FindControl<CalendarDatePicker>("dtTo")!.SelectedDate = now.Date;
FillAccountCombos();
this.FindControl<Button>("btnCompute")!.Click += (_, _) => Compute();
this.FindControl<Button>("btnPdf")!.Click += async (_, _) => await ExportAsync(pdf: true);
this.FindControl<Button>("btnCsv")!.Click += async (_, _) => await ExportAsync(pdf: false);
this.FindControl<Button>("btnLedgerRefresh")!.Click += (_, _) => LoadLedger();
this.FindControl<Button>("btnStatusRefresh")!.Click += (_, _) => LoadRuns();
this.FindControl<Button>("btnIngestNow")!.Click += async (_, _) => await RunIngestAsync(backfill: false);
this.FindControl<Button>("btnBackfill")!.Click += async (_, _) => await RunIngestAsync(backfill: true);
Compute();
LoadLedger();
LoadRuns();
}
private void Status(string text) => this.FindControl<TextBlock>("lblStatus")!.Text = text;
private void FillAccountCombos()
{
var items = new List<AccountChoice> { new(null, "Alle Konten") };
items.AddRange(_state.Accounts.Values.OrderBy(a => a.AccountId)
.Select(a => new AccountChoice(a.AccountId, $"#{a.AccountId} {a.Name}{(a.IsDemo ? " (Demo)" : "")}")));
foreach (var name in new[] { "cbAccount", "cbLedgerAccount", "cbStatusAccount" })
{
var cb = this.FindControl<ComboBox>(name)!;
cb.ItemsSource = items.ToList();
cb.SelectedIndex = 0;
}
}
private int? AccountIdOf(string comboName) =>
(this.FindControl<ComboBox>(comboName)!.SelectedItem as AccountChoice)?.Id;
// ===== Übersicht / BWA =====
private void Compute()
{
try
{
DateTime from = this.FindControl<CalendarDatePicker>("dtFrom")!.SelectedDate?.Date ?? DateTime.Today;
DateTime to = this.FindControl<CalendarDatePicker>("dtTo")!.SelectedDate?.Date ?? DateTime.Today;
int? accountId = AccountIdOf("cbAccount");
string code = this.FindControl<ComboBox>("cbCurrency")!.SelectedItem as string ?? "USDC";
_currency = _reports.ResolveCurrency(code, to);
_statement = _reports.BuildStatement(accountId, from, to);
string unit = _currency.Code;
decimal V(decimal usdc) => AccountingReportService.Convert(usdc, _currency);
this.FindControl<TextBlock>("kpiNet")!.Text = $"{V(_statement.NetTradingResultUsdc):N2} {unit}";
this.FindControl<TextBlock>("kpiClosing")!.Text = $"{V(_statement.ClosingBalanceUsdc):N2} {unit}";
this.FindControl<TextBlock>("kpiDeposits")!.Text = $"{V(_statement.Deposits):N2} {unit}";
this.FindControl<TextBlock>("kpiWithdrawals")!.Text = $"{V(_statement.Withdrawals):N2} {unit}";
this.FindControl<TextBlock>("kpiFees")!.Text = $"{V(_statement.Fees):N2} {unit}";
this.FindControl<TextBlock>("kpiRewards")!.Text = $"{V(_statement.Rewards):N2} {unit}";
this.FindControl<TextBlock>("kpiVolume")!.Text = $"{V(_statement.TradeVolume):N2} {unit}";
this.FindControl<TextBlock>("kpiTrades")!.Text = _statement.TradeCount.ToString();
this.FindControl<TextBlock>("lblCurrencyNote")!.Text = _currency.Note;
_monthly.Clear();
foreach (var m in _reports.BuildMonthly(accountId, from, to))
_monthly.Add(new MonthlyRow
{
Month = m.From.ToString("MM/yyyy"),
Net = $"{V(m.NetTradingResultUsdc):N2}",
Deposits = $"{V(m.Deposits):N2}",
Withdrawals = $"{V(m.Withdrawals):N2}",
Fees = $"{V(m.Fees):N2}",
Volume = $"{V(m.TradeVolume):N2}",
Trades = m.TradeCount,
Closing = $"{V(m.ClosingBalanceUsdc):N2}"
});
Status($"Abrechnung berechnet ({from:dd.MM.yyyy}–{to:dd.MM.yyyy}, {unit}).");
}
catch (Exception ex)
{
Status($"Berechnung fehlgeschlagen: {ex.Message}");
}
}
// ===== Export =====
private async Task ExportAsync(bool pdf)
{
if (_statement == null) { Status("Bitte zuerst berechnen."); return; }
try
{
// Seit der Umstellung auf Seiten ist „this" kein TopLevel mehr – der Dateidialog
// gehört dem Shell-Fenster und wird über den Baum aufgelöst.
var storage = TopLevel.GetTopLevel(this)?.StorageProvider;
if (storage == null) { Status("Kein Fenster für den Dateidialog verfügbar."); return; }
var picker = await storage.SaveFilePickerAsync(new global::Avalonia.Platform.Storage.FilePickerSaveOptions
{
Title = pdf ? "PDF-Export" : "CSV-Export",
SuggestedFileName = $"abrechnung_{_statement.From:yyyyMMdd}_{_statement.To:yyyyMMdd}" + (pdf ? ".pdf" : ".csv"),
DefaultExtension = pdf ? "pdf" : "csv"
});
if (picker == null) return;
string path = picker.Path.LocalPath;
int? accountId = AccountIdOf("cbAccount");
var entries = _ledgerRepo.Query(accountId, _statement.From, _statement.To, 100000);
if (pdf)
{
byte[] bytes = PdfExporter.Render(_statement,
_reports.BuildMonthly(accountId, _statement.From, _statement.To),
entries, _currency.Code, _currency.Factor, _currency.Note);
await File.WriteAllBytesAsync(path, bytes);
}
else
{
await File.WriteAllTextAsync(path, CsvExporter.Statement(_statement) + "\n" + CsvExporter.Ledger(entries));
}
_logger.Info($"Accounting-Export geschrieben: {path}");
Status($"Export gespeichert: {path}");
}
catch (Exception ex)
{
Status($"Export fehlgeschlagen: {ex.Message}");
}
}
// ===== Ledger =====
private void LoadLedger()
{
try
{
_ledger.Clear();
foreach (var e in _ledgerRepo.Query(AccountIdOf("cbLedgerAccount"), null, null, 5000))
_ledger.Add(e);
Status($"{_ledger.Count} Ledger-Sätze geladen.");
}
catch (Exception ex)
{
Status($"Ledger konnte nicht geladen werden: {ex.Message}");
}
}
// ===== Abruf / Status =====
private void LoadRuns()
{
try
{
_runs.Clear();
foreach (var r in _runRepo.GetRecent(AccountIdOf("cbStatusAccount"), 200)) _runs.Add(r);
}
catch (Exception ex)
{
Status($"Abrufhistorie konnte nicht geladen werden: {ex.Message}");
}
}
private async Task RunIngestAsync(bool backfill)
{
var incremental = this.FindControl<Button>("btnIngestNow")!;
var full = this.FindControl<Button>("btnBackfill")!;
incremental.IsEnabled = full.IsEnabled = false;
try
{
Status(backfill ? "Backfill läuft …" : "Inkrementeller Abruf läuft …");
await _ingest.IngestAllAsync(backfill, CancellationToken.None);
LoadRuns();
LoadLedger();
Status($"Abruf abgeschlossen um {DateTime.Now:HH:mm:ss}.");
}
catch (Exception ex)
{
Status($"Abruf fehlgeschlagen: {ex.Message}");
}
finally
{
incremental.IsEnabled = full.IsEnabled = true;
}
}
private sealed record AccountChoice(int? Id, string Label)
{
public override string ToString() => Label;
}
}
}