Add new blazor app

This commit is contained in:
Claudio Schaad 2025-04-29 21:07:24 +02:00
parent 4c598f0827
commit ad75c47709
155 changed files with 64093 additions and 1 deletions

6
.gitignore vendored
View file

@ -51,4 +51,8 @@ CodeCoverage/
# NUnit # NUnit
*.VisualState.xml *.VisualState.xml
TestResult.xml TestResult.xml
nunit-*.xml nunit-*.xml
/AccountingWeb/.vs
/.vs/Accounting
/.vs/ProjectEvaluation
/Schaad.Accounting.UI/.vs

13
.idea/.idea.Accounting/.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.Accounting.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -0,0 +1 @@
Accounting

View file

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>schaad</w>
<w>spendings</w>
</words>
</dictionary>
</component>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

43
Accounting.sln Normal file
View file

@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Schaad.Accounting.UI", "Schaad.Accounting.UI\Schaad.Accounting.UI.csproj", "{18BB46CF-FD35-6A8A-C11E-7F5405927F50}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Schaad.Accounting.Common", "Schaad.Accounting.Common\Schaad.Accounting.Common.csproj", "{ACC60040-DC4A-6690-7600-44585B2B5508}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Schaad.Accounting.Services", "Schaad.Accounting.Services\Schaad.Accounting.Services.csproj", "{0F1A55C2-5D6B-601E-2BB2-0725B5E4C7DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Schaad.Accounting.Db", "Schaad.Accounting.Db\Schaad.Accounting.Db.csproj", "{DC3EEA5C-7075-5F02-00A2-95BCDD87BB59}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{18BB46CF-FD35-6A8A-C11E-7F5405927F50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18BB46CF-FD35-6A8A-C11E-7F5405927F50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18BB46CF-FD35-6A8A-C11E-7F5405927F50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18BB46CF-FD35-6A8A-C11E-7F5405927F50}.Release|Any CPU.Build.0 = Release|Any CPU
{ACC60040-DC4A-6690-7600-44585B2B5508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACC60040-DC4A-6690-7600-44585B2B5508}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACC60040-DC4A-6690-7600-44585B2B5508}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACC60040-DC4A-6690-7600-44585B2B5508}.Release|Any CPU.Build.0 = Release|Any CPU
{0F1A55C2-5D6B-601E-2BB2-0725B5E4C7DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F1A55C2-5D6B-601E-2BB2-0725B5E4C7DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F1A55C2-5D6B-601E-2BB2-0725B5E4C7DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F1A55C2-5D6B-601E-2BB2-0725B5E4C7DA}.Release|Any CPU.Build.0 = Release|Any CPU
{DC3EEA5C-7075-5F02-00A2-95BCDD87BB59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC3EEA5C-7075-5F02-00A2-95BCDD87BB59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC3EEA5C-7075-5F02-00A2-95BCDD87BB59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC3EEA5C-7075-5F02-00A2-95BCDD87BB59}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9C94ED36-E7EA-455C-967A-C293DD9B4C81}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABar_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3d754f61d7ca16fefcc624d522ddfb3235d54b85ab5638c1e181cde5af4357d_003FBar_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003Fschaa_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View file

@ -0,0 +1,10 @@
namespace Schaad.Accounting
{
public class ClassIds
{
public const int Activa = 1;
public const int Passiva = 2;
public const int Income = 3;
public const int Expenses = 4;
}
}

View file

@ -0,0 +1,38 @@
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Datasets
{
public class AccountDataset : Account
{
public decimal Balance { get; }
public decimal BalanceCHF { get; }
public decimal StartBalanceCHF { get; }
public string ClassName { get; }
public string SubClassName { get; }
public AccountDataset(Account account, decimal balance, decimal balanceCHF, decimal startBalanceCHF, string className, string subClassName)
{
LastBankBalance = account.LastBankBalance;
StartBalance = account.StartBalance;
StartBalanceCHF = startBalanceCHF;
BankAccountNumber = account.BankAccountNumber;
Currency = account.Currency;
Id = account.Id;
Name = account.Name;
Number = account.Number;
Balance = balance;
BalanceCHF = balanceCHF;
ClassName = className;
SubClassName = subClassName;
}
public override string ToString()
{
return string.Format("Name: {0}, Number: {1}, Balance {2}", Name, Number, Balance);
}
}
}

View file

@ -0,0 +1,15 @@
using System;
namespace Schaad.Accounting.Datasets
{
public class BankTransactionDataset
{
public string Id { get; set; }
public DateTime BookingDate { get; set; }
public DateTime ValueDate { get; set; }
public decimal Value { get; set; }
public string Text { get; set; }
public string Debtor { get; set; }
public string Creditor { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Datasets
{
public class BookingRuleDataset : BookingRule
{
public string Account { get; set; }
public BookingRuleDataset(BookingRule bookingRule, string account)
{
this.LookupText = bookingRule.LookupText;
this.LookupValue = bookingRule.LookupValue;
this.BookingText = bookingRule.BookingText;
this.Id = bookingRule.Id;
this.AccountId = bookingRule.AccountId;
this.Account = account;
}
}
}

View file

@ -0,0 +1,7 @@
using System;
using System.Collections.Generic;
namespace Schaad.Accounting.Datasets.Charts
{
public record DataSerie(string Id, string Name, IReadOnlyList<DateOnly> X, IReadOnlyList<decimal> Y);
}

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
namespace Schaad.Accounting.Datasets
{
public class MessageDataset
{
public string Title { get; set; }
public List<string> Lines { get; set; }
public MessageStatus Status { get; set; }
public MessageDataset(string title, MessageStatus status = MessageStatus.Success)
{
Title = title;
Status = status;
Lines = new List<string>();
}
public void Add(string message, MessageStatus status = MessageStatus.Success)
{
Lines.Add(message);
if (status > Status)
{
Status = status;
}
}
public string GetCssAlertClass()
{
switch (Status)
{
case MessageStatus.Success: return "alert-success";
case MessageStatus.Info: return "alert-info";
case MessageStatus.Warning: return "alert-warning";
case MessageStatus.Error: return "alert-danger";
default: return "alert-success";
}
}
}
public enum MessageStatus
{
Success = 0,
Info = 1,
Warning = 2,
Error = 3
}
}

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Schaad.Accounting.Datasets.Reports
{
public class BalanceDataset
{
public List<AccountDataset> ActivaAccountList { get; set; }
public List<AccountDataset> PassivaAccountList { get; set; }
public decimal TotalActivaCHF { get; set; }
public decimal TotalPassivaCHF { get; set; }
public BalanceDataset(List<AccountDataset> activaAccountList, List<AccountDataset> passivaAccountList, decimal totalActivaCHF, decimal totalPassivaCHF)
{
ActivaAccountList = activaAccountList;
PassivaAccountList = passivaAccountList;
TotalActivaCHF = totalActivaCHF;
TotalPassivaCHF = totalPassivaCHF;
}
}
}

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Schaad.Accounting.Datasets.Reports
{
public class BalanceSheetDataset
{
public List<AccountDataset> ActivaAccountList { get; }
public List<AccountDataset> IncomeAccountList { get; }
public List<AccountDataset> ExpensesAccountList { get; }
public decimal ProfitCHF { get; }
public decimal LossCHF { get; }
public int Year { get; }
public BalanceSheetDataset(List<AccountDataset> activaAccountList, List<AccountDataset> incomeAccountList, List<AccountDataset> expensesAccountList, decimal profitCHF, decimal lossCHF, int year)
{
ActivaAccountList = activaAccountList;
IncomeAccountList = incomeAccountList;
ExpensesAccountList = expensesAccountList;
ProfitCHF = profitCHF;
LossCHF = lossCHF;
Year = year;
}
}
}

View file

@ -0,0 +1,9 @@
namespace Schaad.Accounting.Datasets
{
public class SettingsDataset
{
public string DataPath { get; set; }
public string FixerIoApiKey { get; set; }
}
}

View file

@ -0,0 +1,37 @@
using System;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Datasets
{
public class TransactionDataset : Transaction
{
public Account OriginAccount { get; set; }
public Account TargetAccount { get; set; }
public TransactionDataset(Transaction transaction, Account originAccount, Account targetAccout)
{
this.BankTransactionId = transaction.BankTransactionId;
this.Id = transaction.Id;
this.OriginAccountId = transaction.OriginAccountId;
this.TargetAccountId = transaction.TargetAccountId;
this.Text = transaction.Text;
this.Value = transaction.Value;
this.FxRate = transaction.FxRate;
this.ValueDate = transaction.ValueDate;
this.BookingDate = transaction.BookingDate;
this.OriginAccount = originAccount;
this.TargetAccount = targetAccout;
}
public decimal GetValue(bool withFxRate)
{
if (withFxRate && FxRate != 0)
{
return Math.Round(Value / FxRate.Value, 2);
}
else
return Value;
}
}
}

View file

@ -0,0 +1,15 @@
using System.Globalization;
using System.Text;
namespace Schaad.Accounting.Common.Extensions
{
public static class DecimalExtension
{
public static string ToFormattedString(this decimal value)
{
var culture = new CultureInfo("de-CH");
culture.NumberFormat.NumberGroupSeparator = "'";
return value.ToString("#,0.00", culture);
}
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface IAccountRepository
{
List<Account> GetAccountList();
void SaveAccount(Account account);
Account GetAccount(string id);
void DeleteAccount(string id);
Account GetAccountForBankAccountNumber(string bankAccountNumber);
void SaveBankAccountBalance(string bankAccountNumber, decimal accountBalance);
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface IBankTransactionRepository
{
List<BankTransaction> GetBankTransactionList();
int SaveBankTransactionList(string bankAccountNumber, List<BankTransactionDataset> bankTransactions);
BankTransaction GetBankTransaction(string id);
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface IBookingRuleRepository
{
List<BookingRule> GetBookingRuleList();
void SaveBookingRule(BookingRule bookingRule);
BookingRule GetBookingRule(string id);
void DeleteBookingRule(string id);
}
}

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface IBookingTextRepository
{
List<BookingText> GetBookingTextList();
void SaveBookingText(BookingText bookingText);
BookingText GetBookingText(string id);
void DeleteBookingText(string id);
}
}

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Schaad.Accounting.Datasets.Charts;
namespace Schaad.Accounting.Interfaces
{
public interface IChartService
{
IReadOnlyList<DataSerie> GetExpensesPerMonth();
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Schaad.Accounting.Datasets;
using Schaad.Finance.Api;
using Schaad.Finance.Api.Datasets;
namespace Schaad.Accounting.Interfaces
{
public interface IFileService
{
string Backup();
IReadOnlyList<MessageDataset> ImportAccountStatementFile(string filePath);
IReadOnlyList<CreditCardTransaction> ImportCreditCardStatementFile(CreditCardProvider creditCardProvider, string filePath);
byte[] GetTransactionListCsv(string accountId);
}
}

View file

@ -0,0 +1,33 @@
using Schaad.Accounting.Datasets;
namespace Schaad.Accounting.Interfaces
{
public interface ISettingsService
{
int GetYear();
void SetYear(int year);
bool TrySetYear(int year);
string GetMandator();
void SetMandator(string mandator);
string GetYearMandatorPath();
string GetLastYearMandatorPath();
string GetBackupFilePath();
string GetUploadPath();
string GetCreditCardStatementPath();
string GetDbPath();
string GetLastYearDbPath();
SettingsDataset GetSettings();
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface ISplitPredefinitonRepository
{
List<SplitPredefinition> GetSplitPredefinitionList();
void SaveSplitPredefinition(SplitPredefinition splitPredefinition);
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface ISubclassRepository
{
List<SubClass> GetSubClassList();
void SaveSubClass(SubClass subClass);
SubClass GetSubClass(string id);
void DeleteSubClass(string id);
Dictionary<int, string> GetClasses();
string GetClass(int classId);
}
}

View file

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Interfaces
{
public interface ITransactionRepository
{
List<Transaction> GetTransactionList();
void SaveTransaction(Transaction transaction);
Transaction GetTransaction(string id);
}
}

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Datasets.Reports;
using Schaad.Accounting.Models;
using Schaad.Finance.Api.Datasets;
namespace Schaad.Accounting.Interfaces
{
public interface IViewService
{
AccountDataset GetAccountView(string id);
List<AccountDataset> GetAccountViewList();
BalanceDataset GetBalanceView();
BalanceSheetDataset GetBalanceSheetView(int year);
List<TransactionDataset> GetTransactionViewList();
List<TransactionDataset> GetTransactionViewList(string accountId);
List<BookingRuleDataset> GetBookingRuleViewList();
List<BankTransaction> GetOpenBankTransactionList();
List<Transaction> MatchOpenBankTransactions();
List<Transaction> MatchCreditCardTransactions(string bankTransactionId, IReadOnlyList<CreditCardTransaction> creditCardTransactions);
}
}

View file

@ -0,0 +1,77 @@
using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace Schaad.Accounting.Models
{
public class Account
{
public string Id { get; set; }
[Display(Name = "Nummer")]
[Required]
public int Number { get; set; }
[Display(Name = "Name")]
[Required]
[MinLength(3)]
public string Name { get; set; }
[Display(Name = "Währung")]
[Required]
[MinLength(3)]
[MaxLength(3)]
public string Currency { get; set; }
[XmlIgnore]
[Display(Name = "Klasse")]
public int Class
{
get { return Number / 1000; }
}
[XmlIgnore]
[Display(Name = "Subklasse")]
public int SubClass
{
get { return Number / 100; }
}
/// <summary>
/// If mapped to a bank account
/// </summary>
[Display(Name = "Bankkontonummer")]
public string BankAccountNumber { get; set; }
/// <summary>
/// Start balance for acitvas
/// </summary>
[Display(Name = "Start Saldo")]
public decimal StartBalance { get; set; }
/// <summary>
/// Last received balance from bank
/// </summary>
[Display(Name = "Letzter Banksaldo")]
public decimal LastBankBalance { get; set; }
[XmlIgnore]
public bool IsFxAccount
{
get { return string.IsNullOrEmpty(Currency) == false && Currency != "CHF"; }
}
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(Account target)
{
target.LastBankBalance = LastBankBalance;
target.StartBalance = StartBalance;
target.BankAccountNumber = BankAccountNumber;
target.Currency = string.IsNullOrEmpty(Currency) ? "CHF" : Currency;
target.Id = Id;
target.Name = Name;
target.Number = Number;
}
}
}

View file

@ -0,0 +1,27 @@
using System;
namespace Schaad.Accounting.Models
{
public class BankTransaction
{
/// <summary>
/// Id
/// </summary>
public string Id { get; set; }
public DateTime ValueDate { get; set; }
public DateTime BookingDate { get; set; }
public string Text { get; set; }
public decimal Value { get; set; }
public string BankAccountNumber { get; set; }
public string Debtor { get; set; }
public string Creditor { get; set; }
public bool Ignore { get; set; }
}
}

View file

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace Schaad.Accounting.Models
{
public class BookingRule
{
public string Id { get; set; }
[Display(Name = "Suchtext")]
[Required]
public string LookupText { get; set; }
[Display(Name = "Suchbetrag")]
public decimal LookupValue { get; set; }
[Display(Name = "Buchungstext")]
[Required]
public string BookingText { get; set; }
[Display(Name = "Konto")]
[Required]
public string AccountId { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(BookingRule target)
{
target.LookupText = LookupText;
target.LookupValue = LookupValue;
target.BookingText = BookingText;
target.Id = Id;
target.AccountId = AccountId;
}
}
}

View file

@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
namespace Schaad.Accounting.Models
{
public class BookingText
{
public string Id { get; set; }
[Display(Name = "Buchungstext")]
[Required]
[MinLength(3)]
public string Text { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(BookingText target)
{
target.Id = Id;
target.Text = Text;
}
}
}

View file

@ -0,0 +1,25 @@
namespace Schaad.Accounting.Models
{
public class SplitPredefinition
{
public string Id { get; set; }
public string BookingText { get; set; }
public decimal BookingValue { get; set; }
public string AccountId { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(SplitPredefinition target)
{
target.BookingText = BookingText;
target.BookingValue = BookingValue;
target.Id = Id;
target.AccountId = AccountId;
}
}
}

View file

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
namespace Schaad.Accounting.Models
{
public class SubClass
{
public string Id { get; set; }
[Display(Name = "Nummer")]
[Required]
public int Number { get; set; }
[Display(Name = "Name")]
[Required]
[MinLength(3)]
public string Name { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(SubClass target)
{
target.Id = Id;
target.Name = Name;
target.Number = Number;
}
}
}

View file

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Schaad.Accounting.Models
{
public class Transaction
{
public string Id { get; set; }
public string BankTransactionId { get; set; }
/// <summary>
/// Needed for booking rule
/// </summary>
public string BankTransactionText { get; set; }
public string RelatedParty { get; set; }
[Display(Name = "Valuta")]
public DateTime ValueDate { get; set; }
[Display(Name = "Buchung")]
public DateTime BookingDate { get; set; }
[Display(Name = "Buchungstext")]
public string Text { get; set; }
[Display(Name = "Betrag")]
public decimal Value { get; set; }
[Display(Name = "Habenkonto")]
public string OriginAccountId { get; set; }
[Display(Name = "Sollkonto")]
public string TargetAccountId { get; set; }
[Display(Name = "Währungskurs")]
public decimal? FxRate { get; set; }
public Transaction()
{
}
public Transaction(BankTransaction bankTransaction, List<Account> accounts)
{
BankTransactionId = bankTransaction.Id;
BankTransactionText = bankTransaction.Text;
Value = bankTransaction.Value;
ValueDate = bankTransaction.ValueDate;
BookingDate = bankTransaction.BookingDate;
// Depending on positiv or negative value, set the account
var bankAccount = accounts.FirstOrDefault(a => a.BankAccountNumber == bankTransaction.BankAccountNumber);
if (Value >= 0)
{
TargetAccountId = bankAccount.Id;
RelatedParty = bankTransaction.Debtor;
}
else
{
OriginAccountId = bankAccount.Id;
RelatedParty = bankTransaction.Creditor;
}
}
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(Transaction target)
{
target.BankTransactionId = BankTransactionId;
target.BankTransactionText = BankTransactionText;
target.Id = Id;
target.OriginAccountId = OriginAccountId;
target.TargetAccountId = TargetAccountId;
target.Text = Text;
target.Value = Value;
target.ValueDate = ValueDate;
target.BookingDate = BookingDate > DateTime.MinValue ? BookingDate : ValueDate;
target.FxRate = FxRate;
}
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Schaad.Accounting</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Schaad.Finance.Api">
<HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class AccountRepository : BaseRepository, IAccountRepository
{
private readonly string ACCOUNTS = "Accounts.xml";
public AccountRepository(ISettingsService settingsService) : base(settingsService)
{
EnsureFileExisits(ACCOUNTS);
}
/// <summary>
/// Load accounts
/// </summary>
public List<Account> GetAccountList()
{
var accounts = Load<List<Account>>(ACCOUNTS);
return accounts ?? new List<Account>();
}
/// <summary>
/// Save an account (insert/update)
/// </summary>
public void SaveAccount(Account account)
{
var accounts = GetAccountList();
var existingAccount = accounts.FirstOrDefault(a => a.Id == account.Id);
if (existingAccount == null)
{
existingAccount = new Account();
accounts.Add(existingAccount);
account.Id = Guid.NewGuid().ToString();
}
account.Copy(existingAccount);
Save(accounts, ACCOUNTS);
}
/// <summary>
/// Get account
/// </summary>
public Account GetAccount(string id)
{
var accounts = GetAccountList();
return accounts.FirstOrDefault(t => t.Id == id);
}
/// <summary>
/// Delete account
/// </summary>
public void DeleteAccount(string id)
{
var accounts = GetAccountList();
accounts.RemoveAll(t => t.Id == id);
Save(accounts, ACCOUNTS);
}
/// <summary>
/// Get account
/// </summary>
public Account GetAccountForBankAccountNumber(string bankAccountNumber)
{
var accounts = GetAccountList();
return accounts.SingleOrDefault(t => t.BankAccountNumber == bankAccountNumber);
}
public void SaveBankAccountBalance(string bankAccountNumber, decimal accountBalance)
{
var account = GetAccountForBankAccountNumber(bankAccountNumber);
account.LastBankBalance = accountBalance;
SaveAccount(account);
}
private new void EnsureFileExisits(string file)
{
string filePath = Path.Combine(settingsService.GetDbPath(), file);
if (File.Exists(filePath) == false)
{
var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), file);
if (File.Exists(lastYearFile))
File.Copy(lastYearFile, filePath);
// set start balance to last bank balance
if (file.IndexOf("Accounts") > -1)
{
var accounts = GetAccountList();
foreach (var account in accounts.Where(a => a.Class == ClassIds.Activa))
{
// bank accounts
if (account.LastBankBalance > 0)
{
account.StartBalance = account.LastBankBalance;
SaveAccount(account);
}
// cash accounts
else if (account.Number < 1010)
{
account.StartBalance = 0;
SaveAccount(account);
}
}
}
}
}
}
}

View file

@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class BankTransactionRepository : BaseRepository, IBankTransactionRepository
{
private readonly string BANK_TRANSACTIONS = "BankTransactions.xml";
public BankTransactionRepository(ISettingsService settingsService) : base(settingsService)
{
}
/// <summary>
/// Load bank transactions
/// </summary>
public List<BankTransaction> GetBankTransactionList()
{
var transactions = Load<List<BankTransaction>>(BANK_TRANSACTIONS);
return transactions != null ? transactions : new List<BankTransaction>();
}
/// <summary>
/// Save new bank transactions, ignore existing ones (no update)
/// </summary>
public int SaveBankTransactionList(string bankAccountNumber, List<BankTransactionDataset> bankTransactions)
{
var count = 0;
var transactions = GetBankTransactionList();
foreach (var bankTrx in bankTransactions)
{
var transaction = transactions.FirstOrDefault(t => t.Id == bankTrx.Id);
if (transaction == null)
{
transaction = new BankTransaction()
{
Id = bankTrx.Id,
BookingDate = bankTrx.BookingDate,
ValueDate = bankTrx.ValueDate,
Value = (decimal)bankTrx.Value,
Text = bankTrx.Text,
BankAccountNumber = bankAccountNumber,
Debtor = bankTrx.Debtor,
Creditor = bankTrx.Creditor,
};
transactions.Add(transaction);
count++;
}
}
Save(transactions, BANK_TRANSACTIONS);
return count;
}
/// <summary>
/// Get bank transaction
/// </summary>
public BankTransaction GetBankTransaction(string id)
{
var transactions = GetBankTransactionList();
return transactions.FirstOrDefault(t => t.Id == id);
}
}
}

View file

@ -0,0 +1,72 @@
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.Repositories
{
public abstract class BaseRepository
{
protected readonly ISettingsService settingsService;
protected BaseRepository(ISettingsService settingsService)
{
this.settingsService = settingsService;
}
protected void EnsureFileExisits(string fileName)
{
string filePath = Path.Combine(settingsService.GetDbPath(), fileName);
if (File.Exists(filePath) == false)
{
var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), fileName);
if (File.Exists(lastYearFile))
{
File.Copy(lastYearFile, filePath);
}
}
}
/// <summary>
/// Save an object to an xml file
/// </summary>
protected void Save<T>(T obj, string fileName)
{
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
using (var sww = new MemoryStream())
{
var settings = new XmlWriterSettings
{
Encoding = Encoding.UTF8,
Indent = true
};
using (var writer = XmlWriter.Create(sww, settings))
{
var xsSubmit = new XmlSerializer(typeof(T));
xsSubmit.Serialize(writer, obj);
var xml = Encoding.UTF8.GetString(sww.ToArray());
File.WriteAllText(filePath, xml);
}
}
}
/// <summary>
/// Load an object from an xml file
/// </summary>
protected T Load<T>(string fileName)
{
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
if (File.Exists(filePath) == false)
{
return default(T);
}
using (XmlReader reader = XmlReader.Create(filePath))
{
var serializer = new XmlSerializer(typeof(T));
return (T)serializer.Deserialize(reader);
}
}
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class BookingRuleRepository : BaseRepository, IBookingRuleRepository
{
private readonly string BOOKING_RULES = "BookingRules.xml";
public BookingRuleRepository(ISettingsService settingsService) : base(settingsService)
{
EnsureFileExisits(BOOKING_RULES);
}
/// <summary>
/// Load booking rules
/// </summary>
public List<BookingRule> GetBookingRuleList()
{
var bookingTexts = Load<List<BookingRule>>(BOOKING_RULES);
return bookingTexts ?? new List<BookingRule>();
}
/// <summary>
/// Save a booking rule (insert/update)
/// </summary>
public void SaveBookingRule(BookingRule bookingRule)
{
var bookingRules = GetBookingRuleList();
var existingRule = bookingRules.FirstOrDefault(a => a.Id == bookingRule.Id);
if (existingRule == null)
{
existingRule = new BookingRule();
bookingRules.Add(existingRule);
bookingRule.Id = Guid.NewGuid().ToString();
}
bookingRule.Copy(existingRule);
Save(bookingRules, BOOKING_RULES);
}
/// <summary>
/// Get booking rule
/// </summary>
public BookingRule GetBookingRule(string id)
{
var bookingRules = GetBookingRuleList();
return bookingRules.FirstOrDefault(t => t.Id == id);
}
/// <summary>
/// Delete booking rule
/// </summary>
public void DeleteBookingRule(string id)
{
var bookingRules = GetBookingRuleList();
bookingRules.RemoveAll(t => t.Id == id);
Save(bookingRules, BOOKING_RULES);
}
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class BookingTextRepository : BaseRepository, IBookingTextRepository
{
private readonly string BOOKING_TEXTS = "BookingTexts.xml";
public BookingTextRepository(ISettingsService settingsService) : base(settingsService)
{
EnsureFileExisits(BOOKING_TEXTS);
}
/// <summary>
/// Load bokking texts
/// </summary>
public List<BookingText> GetBookingTextList()
{
var bookingTexts = Load<List<BookingText>>(BOOKING_TEXTS);
return bookingTexts ?? new List<BookingText>();
}
/// <summary>
/// Save a booking text (insert/update)
/// </summary>
public void SaveBookingText(BookingText bookingText)
{
var bookingTexts = GetBookingTextList();
var existingText = bookingTexts.FirstOrDefault(a => a.Id == bookingText.Id);
if (existingText == null)
{
existingText = new BookingText();
bookingTexts.Add(existingText);
bookingText.Id = Guid.NewGuid().ToString();
}
bookingText.Copy(existingText);
Save(bookingTexts, BOOKING_TEXTS);
}
/// <summary>
/// Get booking text
/// </summary>
public BookingText GetBookingText(string id)
{
var bookingTexts = GetBookingTextList();
return bookingTexts.FirstOrDefault(t => t.Id == id);
}
/// <summary>
/// Delete booking text
/// </summary>
public void DeleteBookingText(string id)
{
var bookingTexts = GetBookingTextList();
bookingTexts.RemoveAll(t => t.Id == id);
Save(bookingTexts, BOOKING_TEXTS);
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class SplitPredefinitonRepository : BaseRepository, ISplitPredefinitonRepository
{
private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml";
public SplitPredefinitonRepository(ISettingsService settingsService) : base(settingsService)
{
EnsureFileExisits(SPLIT_PREDEFINITION);
}
/// <summary>
/// Load booking rules
/// </summary>
public List<SplitPredefinition> GetSplitPredefinitionList()
{
var definitions = Load<List<SplitPredefinition>>(SPLIT_PREDEFINITION);
return definitions ?? new List<SplitPredefinition>();
}
/// <summary>
/// Save a booking rule (insert/update)
/// </summary>
public void SaveSplitPredefinition(SplitPredefinition splitPredefinition)
{
var definitions = GetSplitPredefinitionList();
var existingDefinition = definitions.FirstOrDefault(a => a.Id == splitPredefinition.Id);
if (existingDefinition == null)
{
existingDefinition = new SplitPredefinition();
definitions.Add(existingDefinition);
splitPredefinition.Id = Guid.NewGuid().ToString();
}
splitPredefinition.Copy(existingDefinition);
Save(definitions, SPLIT_PREDEFINITION);
}
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class SubclassRepository : BaseRepository, ISubclassRepository
{
private readonly Dictionary<int, string> classes = new Dictionary<int, string>();
private readonly string SUBCLASSES = "SubClasses.xml";
public SubclassRepository(ISettingsService settingsService) : base(settingsService)
{
EnsureFileExisits(SUBCLASSES);
classes.Add(ClassIds.Activa, "Aktiven");
classes.Add(ClassIds.Passiva, "Passiven");
classes.Add(ClassIds.Income, "Einnahmen");
classes.Add(ClassIds.Expenses, "Ausgaben");
}
/// <summary>
/// Load bokking texts
/// </summary>
public List<SubClass> GetSubClassList()
{
var subclasses = Load<List<SubClass>>(SUBCLASSES);
return subclasses ?? new List<SubClass>();
}
/// <summary>
/// Save a booking text (insert/update)
/// </summary>
public void SaveSubClass(SubClass subClass)
{
var subclasses = GetSubClassList();
var existingSubClass = subclasses.FirstOrDefault(a => a.Id == subClass.Id);
if (existingSubClass == null)
{
existingSubClass = new SubClass();
subclasses.Add(existingSubClass);
subClass.Id = Guid.NewGuid().ToString();
}
subClass.Copy(existingSubClass);
Save(subclasses, SUBCLASSES);
}
/// <summary>
/// Get subclass
/// </summary>
public SubClass GetSubClass(string id)
{
var subclasses = GetSubClassList();
return subclasses.FirstOrDefault(t => t.Id == id);
}
public void DeleteSubClass(string id)
{
var subclasses = GetSubClassList();
subclasses.RemoveAll(t => t.Id == id);
Save(subclasses, SUBCLASSES);
}
public Dictionary<int, string> GetClasses()
{
return classes;
}
public string GetClass(int classId)
{
return classes[classId];
}
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.Repositories
{
public class TransactionRepository : BaseRepository, ITransactionRepository
{
private readonly IAccountRepository accountRepository;
private readonly string TRANSACTIONS = "Transactions.xml";
public TransactionRepository(ISettingsService settingsService, IAccountRepository accountRepository) : base(settingsService)
{
this.accountRepository = accountRepository;
}
/// <summary>
/// Get transaction list
/// </summary>
public List<Transaction> GetTransactionList()
{
var transactionList = Load<List<Transaction>>(TRANSACTIONS);
return transactionList ?? new List<Transaction>();
}
/// <summary>
/// Save a transaction (insert/update)
/// </summary>
public void SaveTransaction(Transaction transaction)
{
// prevent saving fxrate for chf account
var isFxAccount = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount;
// if source account is fx account, we pass a foreign value -> calc to chf
if (isFxAccount && transaction.FxRate != 0)
{
transaction.Value = transaction.Value * transaction.FxRate.Value;
}
isFxAccount |= accountRepository.GetAccount(transaction.TargetAccountId).IsFxAccount;
if (isFxAccount == false)
{
transaction.FxRate = null;
}
var transactionList = GetTransactionList();
var existingTransaction = transactionList.FirstOrDefault(a => a.Id == transaction.Id);
if (existingTransaction == null)
{
existingTransaction = new Transaction();
transactionList.Add(existingTransaction);
transaction.Id = Guid.NewGuid().ToString();
}
transaction.Copy(existingTransaction);
Save(transactionList, TRANSACTIONS);
}
/// <summary>
/// Get transaction
/// </summary>
public Transaction GetTransaction(string id)
{
var transactions = GetTransactionList();
var transaction = transactions.FirstOrDefault(t => t.Id == id);
// if source account is fx account, we save in chf currency -> calc to foreign currency for display
var isFxAccount = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount;
if (isFxAccount && transaction.FxRate != 0)
{
transaction.Value = transaction.Value / transaction.FxRate.Value;
}
return transaction;
}
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Datasets.Charts;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.Services
{
public class ChartService : IChartService
{
private readonly IAccountRepository accountRepository;
private readonly ISettingsService settingsService;
private readonly ISubclassRepository subclassRepository;
private readonly IViewService viewService;
public ChartService(
ISettingsService settingsService,
IViewService viewService,
IAccountRepository accountRepository,
ISubclassRepository subclassRepository)
{
this.settingsService = settingsService;
this.viewService = viewService;
this.accountRepository = accountRepository;
this.subclassRepository = subclassRepository;
}
public IReadOnlyList<DataSerie> GetExpensesPerMonth()
{
var transactions = viewService.GetTransactionViewList().Where(a => a.TargetAccount.Class == ClassIds.Expenses);
// More than one subclass xx
if (transactions.Select(t => t.TargetAccount.SubClass).Distinct().Count() > 1)
{
return GetSubClassExpensesPerMonth();
}
// only one subclass xx (z.B. Mandant Mannenbach)
return GetAccountExpensesPerMonth();
}
private List<DataSerie> GetSubClassExpensesPerMonth()
{
//var subclasses = subclassRepository.GetSubClassList();
var transactions = viewService.GetTransactionViewList().Where(a => a.TargetAccount.Class == ClassIds.Expenses).ToList();
var list = new List<DataSerie>();
var newestTransaction = transactions.OrderByDescending(t => t.ValueDate).FirstOrDefault();
var maxMonth = newestTransaction?.ValueDate.Month ?? 12;
var year = settingsService.GetYear();
// Group by subclass
foreach (var grp in transactions.GroupBy(a => a.TargetAccount.SubClass).Select(a => new {Key = a.Key, List = a.ToList()}))
{
// Sum subclass transactions per month
var groupedByMonth = grp.List.GroupBy(g => g.ValueDate.Month).ToDictionary(g => g.Key, g => g.ToList().Sum(s => s.Value));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
//var subClass = subclasses.FirstOrDefault(s => s.Number == grp.List.First().TargetAccount.SubClass);
list.Add(
new DataSerie(
Id: grp.List.First().TargetAccount.SubClass.ToString(),
Name: grp.List.First().TargetAccount.Name,
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
)
);
}
return list;
}
private List<DataSerie> GetAccountExpensesPerMonth()
{
var list = new List<DataSerie>();
var expensesAccounts = viewService.GetAccountViewList().Where(a => a.Class == ClassIds.Expenses);
foreach (var account in expensesAccounts)
{
var serie = GetAccountExpensesPerMonth(account.Id, DateTime.Now.Year);
if (serie != null)
{
list.Add(serie);
}
}
return list;
}
private DataSerie GetAccountExpensesPerMonth(string accountId, int year)
{
if (settingsService.TrySetYear(year))
{
var accountList = accountRepository.GetAccountList();
var account = accountList.SingleOrDefault(a => a.Id == accountId);
// perhaps we dont have the account for last year
if (account == null)
{
return null;
}
var transactions = viewService.GetTransactionViewList().Where(t => t.TargetAccountId == accountId).ToList();
var newestTransaction = transactions.OrderByDescending(t => t.ValueDate).FirstOrDefault();
var maxMonth = newestTransaction?.ValueDate.Month ?? 12;
var groupedByMonth = transactions.GroupBy(g => g.ValueDate.Month).ToDictionary(g => g.Key, g => g.ToList().Sum(s => s.Value));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
settingsService.SetYear(DateTime.Now.Year);
return new DataSerie(
Id: accountId,
Name: account.Name,
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
);
}
return null;
}
private void EnsureEntryForEveryMonth(Dictionary<int, decimal> values, int maxMonth = 12)
{
for (int i = 1; i <= maxMonth; i++)
{
if (values.ContainsKey(i) == false)
{
values.Add(i, 0);
}
}
}
}
}

View file

@ -0,0 +1,26 @@
using Schaad.Finance.Api;
using System;
using System.Collections.Generic;
namespace Schaad.Accounting.Services
{
public class DummyFxService : IFxService
{
private List<string> currencies = ["CHF", "EUR", "USD"];
public decimal ConvertCurrency(decimal amount, string fromCurrency, string toCurrency, string fixerIoApiKey)
{
if (currencies.Contains(fromCurrency) && currencies.Contains(fromCurrency))
{
return amount;
}
throw new NotImplementedException($"Cannot convert from {fromCurrency} to {toCurrency}");
}
public decimal GetFxRate(string fromCurrency, string toCurrency, DateTime date, string fixerIoApiKey)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,150 @@
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Text;
using Schaad.Accounting.Common.Extensions;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
using Schaad.Finance.Api;
using Schaad.Finance.Api.Datasets;
namespace Schaad.Accounting.Services
{
public class FileService : IFileService
{
private readonly IAccountRepository accountRepository;
private readonly ITransactionRepository transactionsRepository;
private readonly IBankTransactionRepository bankTransactionRepository;
private readonly ISettingsService settingsService;
private readonly IAccountStatementService accountStatementService;
private readonly ICreditCardStatementService creditCardStatementService;
public FileService(
ISettingsService settingsService,
IAccountRepository accountRepository,
ITransactionRepository transactionsRepository,
IBankTransactionRepository bankTransactionRepository,
IAccountStatementService accountStatementService,
ICreditCardStatementService creditCardStatementService)
{
this.settingsService = settingsService;
this.accountRepository = accountRepository;
this.transactionsRepository = transactionsRepository;
this.bankTransactionRepository = bankTransactionRepository;
this.accountStatementService = accountStatementService;
this.creditCardStatementService = creditCardStatementService;
}
public string Backup()
{
var outpath = settingsService.GetBackupFilePath();
var sourcepath = settingsService.GetYearMandatorPath();
ZipFile.CreateFromDirectory(sourcepath, outpath);
return $"Backup '{outpath}' erfolgreich erstellt";
}
// Upload account statement file (mt940, camt053)
// http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6
public IReadOnlyList<MessageDataset> ImportAccountStatementFile(string filePath)
{
var messages = new List<MessageDataset>();
var accountStatementResults = accountStatementService.ReadFile(filePath, Encoding.UTF8);
var accountList = accountRepository.GetAccountList().Select(a => a.BankAccountNumber).ToList();
foreach (var accountStatementResult in accountStatementResults)
{
var account = accountStatementResult.AccountStatement;
if (accountStatementResult.IsSuccess == false)
{
messages.Add(new MessageDataset($"Account {account.AccountNumber} NICHT importiert: {accountStatementResult.Error}", MessageStatus.Error));
}
if (accountStatementResult.AccountStatement.Transactions.Any() == false)
{
continue;
}
var message = new MessageDataset($"Konto {account.AccountNumber}");
messages.Add(message);
// only accounts that are in select mandator
if (accountList.Contains(account.AccountNumber))
{
var transactionsThisYear = account.Transactions
.Where(t => t.BookingDate.Year == settingsService.GetYear())
.Select(
t => new BankTransactionDataset
{
Id = t.Id,
Value = (decimal)t.Value,
ValueDate = t.ValueDate,
BookingDate = t.BookingDate,
Text = t.Text,
Debtor = t.Debtor,
Creditor = t.Creditor
})
.ToList();
var count = bankTransactionRepository.SaveBankTransactionList(account.AccountNumber, transactionsThisYear);
accountRepository.SaveBankAccountBalance(account.AccountNumber, (decimal)account.EndBalance.Value);
var status = count == account.Transactions.Count ? MessageStatus.Success : MessageStatus.Info;
message.Add($"{count} von {account.Transactions.Count} Transaktion(en) importiert.", status);
// not imported transactions
var notImportedTransactions = account.Transactions.Where(t => transactionsThisYear.Select(tt => tt.Id).Contains(t.Id) == false);
foreach (var trx in notImportedTransactions)
{
message.Add($"Transaktion wegen BookingDatum {trx.BookingDate} nicht importiert: {trx.Text}", MessageStatus.Warning);
}
}
else
{
message.Add($"Falscher Mandant: {account.Transactions.Count} Transaktion(en) nicht importiert.", MessageStatus.Warning);
}
}
return messages;
}
public IReadOnlyList<CreditCardTransaction> ImportCreditCardStatementFile(CreditCardProvider creditCardProvider, string filePath)
{
var creditCardTransactions = creditCardStatementService.ReadFile(creditCardProvider, filePath, Encoding.UTF8);
return creditCardTransactions;
}
public byte[] GetTransactionListCsv(string accountId)
{
var sb = new StringBuilder();
var account = accountRepository.GetAccount(accountId);
var transactions = transactionsRepository.GetTransactionList()
.Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId)
.OrderBy(t => t.BookingDate)
.ThenBy(t => t.ValueDate)
.ThenBy(t => t.Value)
.ToList();
var balance = account.StartBalance;
sb.AppendLine($"Buchungsdatum;Valuta;Buchungstext;Belastung;Gutschrift;Saldo {account.Currency}");
sb.AppendLine($";;Startsaldo;;;{balance}");
foreach (var trx in transactions)
{
var credit = "";
var debit = "";
if (trx.OriginAccountId == accountId)
{
debit = trx.Value.ToFormattedString();
trx.Value *= -1;
}
else
{
credit = trx.Value.ToFormattedString();
}
balance += trx.Value;
sb.AppendLine($"{trx.BookingDate:dd.MM.yyyy};{trx.ValueDate:dd.MM.yyyy};{trx.Text};{debit};{credit};{balance.ToFormattedString()}");
}
var fileBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(sb.ToString());
return fileBytes;
}
}
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FreeSpire.PDF" Version="10.2.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Schaad.Finance">
<HintPath>..\dlls\Schaad.Finance.dll</HintPath>
</Reference>
<Reference Include="Schaad.Finance.Api">
<HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,124 @@
using System;
using System.IO;
using System.Linq;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.Services
{
public class SettingsService : ISettingsService
{
private readonly SettingsDataset settingsDataset;
private string mandator = "Claudio Schaad";
private int year = DateTime.Now.Year;
public SettingsService(SettingsDataset settingsDataset)
{
this.settingsDataset = settingsDataset;
}
public int GetYear()
{
return year;
}
public void SetYear(int year)
{
this.year = year;
}
public bool TrySetYear(int year)
{
var oldYear = year;
SetYear(year);
if (Directory.GetFiles(GetDbPath()).Any() == false)
{
SetYear(oldYear);
return false;
}
return true;
}
public string GetMandator()
{
return mandator;
}
public void SetMandator(string mandator)
{
this.mandator = mandator;
}
public string GetYearMandatorPath()
{
var path = Path.Combine(GetSettings().DataPath, year.ToString(), mandator);
if (Directory.Exists(path) == false)
{
Directory.CreateDirectory(path);
}
return path;
}
public string GetLastYearMandatorPath()
{
var path = Path.Combine(GetSettings().DataPath, (year - 1).ToString(), mandator);
return path;
}
public string GetBackupFilePath()
{
var backupPath = Path.Combine(GetSettings().DataPath, year.ToString(), "backup", mandator);
if (Directory.Exists(backupPath) == false)
{
Directory.CreateDirectory(backupPath);
}
var backupFilePath = Path.Combine(backupPath, DateTime.Now.ToString("yyyyMMddHHmmss") + ".zip");
return backupFilePath;
}
public string GetUploadPath()
{
var uploadPath = Path.Combine(GetYearMandatorPath(), "uploads");
if (Directory.Exists(uploadPath) == false)
{
Directory.CreateDirectory(uploadPath);
}
return uploadPath;
}
public string GetCreditCardStatementPath()
{
var path = Path.Combine(GetUploadPath(), "creditCard");
if (Directory.Exists(path) == false)
{
Directory.CreateDirectory(path);
}
return path;
}
public string GetDbPath()
{
var path = Path.Combine(GetYearMandatorPath(), "Data");
if (Directory.Exists(path) == false)
{
Directory.CreateDirectory(path);
}
return path;
}
public string GetLastYearDbPath()
{
var path = Path.Combine(GetLastYearMandatorPath(), "Data");
if (Directory.Exists(path) == false)
{
Directory.CreateDirectory(path);
}
return path;
}
public SettingsDataset GetSettings()
{
return settingsDataset;
}
}
}

View file

@ -0,0 +1,355 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Datasets.Reports;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Finance.Api;
using Schaad.Finance.Api.Datasets;
namespace Schaad.Accounting.Services
{
public class ViewService : IViewService
{
private readonly IAccountRepository accountRepository;
private readonly IBankTransactionRepository bankTransactionRepository;
private readonly IBookingRuleRepository bookingRuleRepository;
private readonly ISubclassRepository subclassRepository;
private readonly ITransactionRepository transactionRepository;
private readonly IFxService fxService;
private readonly ISettingsService settingsService;
public ViewService(
IAccountRepository accountRepository,
IBankTransactionRepository bankTransactionRepository,
ITransactionRepository transactionRepository,
ISubclassRepository subclassRepository,
IBookingRuleRepository bookingRuleRepository,
IFxService fxService,
ISettingsService settingsService)
{
this.accountRepository = accountRepository;
this.bankTransactionRepository = bankTransactionRepository;
this.transactionRepository = transactionRepository;
this.subclassRepository = subclassRepository;
this.bookingRuleRepository = bookingRuleRepository;
this.fxService = fxService;
this.settingsService = settingsService;
}
/// <summary>
/// Load accounts
/// </summary>
public AccountDataset GetAccountView(string id)
{
var accounts = GetAccountViewList();
return accounts.Single(t => t.Id == id);
}
/// <summary>
/// Load accounts and calculates their balances
/// </summary>
public List<AccountDataset> GetAccountViewList()
{
var accounts = accountRepository.GetAccountList();
var transactionList = GetTransactionViewList();
var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name);
var settings = settingsService.GetSettings();
var accountList = accounts.Select(
a =>
new AccountDataset(
account: a,
balance: GetBalanceInAccountCurrency(a, transactionList),
balanceCHF: GetCurrentBalanceInCHF(a, transactionList),
startBalanceCHF: fxService.ConvertCurrency(a.StartBalance, a.Currency, "CHF", settings.FixerIoApiKey),
className: subclassRepository.GetClass(a.Class),
subClassName: subClassNameByNumber[a.SubClass]
)
)
.ToList();
return accountList.OrderBy(a => a.Number).ToList();
}
public BalanceDataset GetBalanceView()
{
var accountList = GetAccountViewList();
var balanceView = new BalanceDataset(
activaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(),
passivaAccountList: accountList.Where(m => m.Class == ClassIds.Passiva).ToList(),
totalActivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => m.BalanceCHF)),
totalPassivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Passiva).Sum(m => m.BalanceCHF))
);
return balanceView;
}
public BalanceSheetDataset GetBalanceSheetView(int year)
{
var accountList = GetAccountViewList();
var settings = settingsService.GetSettings();
var profit = Math.Abs(accountList.Where(m => m.Class == ClassIds.Income).Sum(m =>m.BalanceCHF));
profit += Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => fxService.ConvertCurrency(m.StartBalance, m.Currency, "CHF", settings.FixerIoApiKey)));
var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF));
var balanceView = new BalanceSheetDataset(
activaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(),
incomeAccountList: accountList.Where(m => m.Class == ClassIds.Income).ToList(),
expensesAccountList: accountList.Where(m => m.Class == ClassIds.Expenses).ToList(),
profitCHF: profit,
lossCHF: loss,
year: year
);
return balanceView;
}
private decimal GetBalanceInAccountCurrency(Account a, List<TransactionDataset> transactionList)
{
var balance = a.StartBalance
+ transactionList.Where(t => t.TargetAccountId == a.Id).Sum(t => t.GetValue(a.IsFxAccount))
- transactionList.Where(t => t.OriginAccountId == a.Id).Sum(t => t.GetValue(a.IsFxAccount));
return balance;
}
private decimal GetCurrentBalanceInCHF(Account a, List<TransactionDataset> transactionList)
{
var settings = settingsService.GetSettings();
var balanceInAccountCurrency = GetBalanceInAccountCurrency(a, transactionList);
var balanceInChf = fxService.ConvertCurrency(balanceInAccountCurrency, a.Currency, "CHF", settings.FixerIoApiKey);
return balanceInChf;
}
/// <summary>
/// Get transaction list with the origin and target account for each transaction
/// </summary>
public List<TransactionDataset> GetTransactionViewList()
{
var accountList = accountRepository.GetAccountList();
var transactionList = transactionRepository.GetTransactionList();
var transactionViewList = transactionList.Select(
t =>
new TransactionDataset(
t,
accountList.Single(a => a.Id == t.OriginAccountId),
accountList.Single(a => a.Id == t.TargetAccountId)
)
)
.ToList();
return transactionViewList;
}
/// <summary>
/// Get transaction list with the origin and target account for each transaction
/// </summary>
public List<TransactionDataset> GetTransactionViewList(string accountId)
{
var accountList = accountRepository.GetAccountList();
var transactionList = transactionRepository.GetTransactionList().Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId);
var transactionViewList = transactionList.Select(
t =>
new TransactionDataset(
t,
accountList.Single(a => a.Id == t.OriginAccountId),
accountList.Single(a => a.Id == t.TargetAccountId)
)
)
.ToList();
return transactionViewList;
}
/// <summary>
/// Get booking rules with their account
/// </summary>
public List<BookingRuleDataset> GetBookingRuleViewList()
{
var accountList = accountRepository.GetAccountList();
var bookinRuleList = bookingRuleRepository.GetBookingRuleList();
return bookinRuleList.Select(
t =>
new BookingRuleDataset(
t,
accountList.Single(a => a.Id == t.AccountId).Name
)
)
.ToList();
}
/// <summary>
/// Load open bank transactions
/// </summary>
public List<BankTransaction> GetOpenBankTransactionList()
{
var transactions = transactionRepository
.GetTransactionList()
.Where(t => string.IsNullOrEmpty(t.BankTransactionId) == false)
.Select(t => t.BankTransactionId);
var bankTransactions = bankTransactionRepository
.GetBankTransactionList()
.Where(b => b.Ignore == false && transactions.Contains(b.Id) == false)
.ToList();
return bankTransactions;
}
public List<Transaction> MatchOpenBankTransactions()
{
var newTransactionList = new List<Transaction>();
var transactions = transactionRepository.GetTransactionList();
var accounts = accountRepository.GetAccountList();
var bookingRules = bookingRuleRepository.GetBookingRuleList();
var bankTransactions = GetOpenBankTransactionList().OrderBy(t => t.ValueDate);
foreach (var bankTransaction in bankTransactions)
{
var trx = new Transaction(bankTransaction, accounts);
MatchBankTransactionByBookingRule(bankTransaction.Text, trx, bookingRules);
MatchBankTransactionBySameAccountsLastMonth(bankTransaction, trx, transactions);
MatchBankTransactionBySameValueLastMonth(bankTransaction, trx, transactions);
newTransactionList.Add(trx);
}
return newTransactionList;
}
public List<Transaction> MatchCreditCardTransactions(string bankTransactionId, IReadOnlyList<CreditCardTransaction> creditCardTransactions)
{
var accounts = accountRepository.GetAccountList();
var bankTrx = bankTransactionRepository.GetBankTransaction(bankTransactionId);
var bookingRules = bookingRuleRepository.GetBookingRuleList();
var trxList = new List<Transaction>();
foreach (var creditCardTransaction in creditCardTransactions)
{
var trx = new Transaction(bankTrx, accounts);
trx.Value = -1 * creditCardTransaction.Amount;
trx.BookingDate = bankTrx.BookingDate;
trx.ValueDate = creditCardTransaction.TransactionDate;
MatchBankTransactionByBookingRule(creditCardTransaction.Transaction, trx, bookingRules);
if (string.IsNullOrEmpty(trx.Text))
{
trx.Text = creditCardTransaction.Transaction;
}
trxList.Add(trx);
}
return trxList;
}
private void MatchBankTransactionByBookingRule(string transactionText, Transaction trx, List<BookingRule> bookingRules)
{
if (string.IsNullOrEmpty(trx.Text) == false)
return;
// all booking rules that match the text
var matchedRules = bookingRules.Where(r => transactionText.ToLowerInvariant().Contains(r.LookupText.ToLowerInvariant())).ToList();
// then match by value
var matchedRule = matchedRules.FirstOrDefault(r => r.LookupValue == Math.Abs(trx.Value));
// if no found, take match without a value
if (matchedRule == null)
{
matchedRule = matchedRules.FirstOrDefault(r => r.LookupValue == 0);
}
if (matchedRule != null)
{
if (string.IsNullOrEmpty(trx.OriginAccountId))
{
trx.OriginAccountId = matchedRule.AccountId;
}
else
{
trx.TargetAccountId = matchedRule.AccountId;
}
trx.Text = matchedRule.BookingText;
}
}
private void MatchBankTransactionBySameAccountsLastMonth(BankTransaction bankTransaction, Transaction trx, List<Transaction> transactions)
{
if (string.IsNullOrEmpty(trx.Text) == false)
return;
var lastMonth = trx.ValueDate.AddMonths(-1);
var lastMonthStart = new DateTime(lastMonth.Year, lastMonth.Month, 1);
var lastMonthEnd = lastMonthStart.AddMonths(1).AddMinutes(-1);
// Find all bookings for the same accounts of the last month
var lastMonthTransactions = transactions.Where(
t =>
t.OriginAccountId == trx.OriginAccountId &&
t.TargetAccountId == trx.TargetAccountId &&
Math.Abs(t.Value) == Math.Abs(trx.Value) &&
t.ValueDate >= lastMonthStart &&
t.ValueDate <= lastMonthEnd)
.ToList();
MatchSameOrOtherDay(trx, lastMonthTransactions);
}
private void MatchBankTransactionBySameValueLastMonth(BankTransaction bankTransaction, Transaction trx, List<Transaction> transactions)
{
if (string.IsNullOrEmpty(trx.Text) == false)
return;
var lastMonth = trx.ValueDate.AddMonths(-1);
var lastMonthStart = new DateTime(lastMonth.Year, lastMonth.Month, 1);
var lastMonthEnd = lastMonthStart.AddMonths(1).AddMinutes(-1);
// Find all bookings for the same amount of the last month
var lastMonthTransactions = transactions.Where(
t =>
Math.Abs(t.Value) == Math.Abs(trx.Value) &&
t.ValueDate >= lastMonthStart &&
t.ValueDate <= lastMonthEnd)
.ToList();
MatchSameOrOtherDay(trx, lastMonthTransactions);
}
private void MatchSameOrOtherDay(Transaction trx, List<Transaction> lastMonthTransactions)
{
if (lastMonthTransactions.Any())
{
var lastMonthTransaction = lastMonthTransactions.Where(
t =>
t.ValueDate.Day == trx.ValueDate.Day)
.FirstOrDefault();
if (lastMonthTransaction == null)
{
lastMonthTransaction = lastMonthTransactions.Where(
t =>
t.ValueDate.Day >= trx.ValueDate.AddDays(-1).Day &&
t.ValueDate.Day <= trx.ValueDate.AddDays(1).Day)
.FirstOrDefault();
}
if (lastMonthTransaction != null)
{
trx.Text = lastMonthTransaction.Text;
if (string.IsNullOrEmpty(trx.OriginAccountId))
{
trx.OriginAccountId = lastMonthTransaction.OriginAccountId;
}
else
{
trx.TargetAccountId = lastMonthTransaction.TargetAccountId;
}
}
}
}
}
}

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["Schaad.Accounting.UI.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View file

@ -0,0 +1,29 @@
@inherits LayoutComponentBase
<FluentLayout>
<FluentHeader Class="hidePrint">
AccountingNext
</FluentHeader>
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<NavMenu />
<FluentBodyContent Class="body-content">
<div class="content">
@Body
</div>
</FluentBodyContent>
</FluentStack>
<FluentFooter Class="hidePrint">
AccountingNext App
<FluentSpacer/>
Copyright 2025 by Claudio Schaad
</FluentFooter>
</FluentLayout>
<FluentDialogProvider @rendermode="InteractiveServer"/>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

View file

@ -0,0 +1,98 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View file

@ -0,0 +1,35 @@
@using Microsoft.FluentUI.AspNetCore.Components.Icons.Regular
@rendermode InteractiveServer
<!-- https://www.fluentui-blazor.net/Icon#explorer -->
<div class="navmenu hidePrint">
<nav class="sitenav" aria-labelledby="main-menu">
<FluentNavMenu Id="main-menu" Collapsible="true" Width="250" Title="Navigation menu" @bind-Expanded="expanded" CustomToggle="true">
<FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Size20.Home())" IconColor="Color.Accent">Home</FluentNavLink>
<FluentNavLink Href="transactions" Icon="@(new Size20.NumberSymbolSquare())" IconColor="Color.Accent">Transaktionen</FluentNavLink>
<FluentNavLink Href="banktransactions" Icon="@(new Size20.BuildingBank())" IconColor="Color.Accent">Bank Transaktionen</FluentNavLink>
<FluentNavGroup Title="Stammdaten" Icon="@(new Size32.BookDatabase())">
<FluentNavLink Href="bookingrules" Icon="@(new Size20.Check())" IconColor="Color.Accent">Buchungsregeln</FluentNavLink>
<FluentNavLink Href="bookingtexts" Icon="@(new Size32.Text())" IconColor="Color.Accent">Buchungstexte</FluentNavLink>
<FluentNavLink Href="Classes" Icon="@(new Icons.Filled.Size20.Class())" IconColor="Color.Accent">Klassen</FluentNavLink>
<FluentNavLink Href="Accounts" Icon="@(new Size20.PersonAccounts())" IconColor="Color.Accent">Konten</FluentNavLink>
</FluentNavGroup>
<FluentNavGroup Title="Charts" Icon="@(new Size32.ChartMultiple())">
<FluentNavLink Href="spendings" Icon="@(new Size32.DataPie())" IconColor="Color.Accent">Ausgaben</FluentNavLink>
<FluentNavLink Href="SpendingsOverTime" Icon="@(new Size32.DataArea())" IconColor="Color.Accent">Zeitverlauf</FluentNavLink>
<FluentNavLink Href="Assets" Icon="@(new Size32.DataPie())" IconColor="Color.Accent">Vermögen</FluentNavLink>
</FluentNavGroup>
<FluentNavGroup Title="Reports" Icon="@(new Size32.DocumentPrint())">
<FluentNavLink Href="ProfitLossReport" Icon="@(new Size32.DataTrending())" IconColor="Color.Accent">Erfolgsrechnung</FluentNavLink>
<FluentNavLink Href="DetailReport" Icon="@(new Size32.Feed())" IconColor="Color.Accent">Detailaufstellung</FluentNavLink>
<FluentNavLink Href="BalanceReport" Icon="@(new Size32.Feed())" IconColor="Color.Accent">Bilanz</FluentNavLink>
<FluentNavLink Href="BalanceSheetReport" Icon="@(new Size32.Receipt())" IconColor="Color.Accent">Jahresabschluss</FluentNavLink>
</FluentNavGroup>
</FluentNavMenu>
</nav>
</div>
@code {
private bool expanded = true;
}

View file

@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View file

@ -0,0 +1,30 @@
@page "/Accounts"
@rendermode InteractiveServer
<PageTitle>Konten</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neues Konto hinzufügen</FluentButton>
<FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@accountQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.Class)" Sortable="true" Width="100px" />
<PropertyColumn Property="@(p => p.SubClass)" Sortable="true" Width="140px" />
<PropertyColumn Property="@(p => p.Number)" Sortable="true" IsDefaultSortColumn="true" Width="140px" />
<PropertyColumn Property="@(p => p.Name)" Sortable="true" />
<PropertyColumn Property="@(p => p.Currency)" />
<PropertyColumn Property="@(p => p.BankAccountNumber)" />
<PropertyColumn Property="@(p => p.StartBalance)" Format="C2"/>
<PropertyColumn Property="@(p => p.LastBankBalance)" Format="C2" />
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))" />
</TemplateColumn>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Edit())" Appearance="Appearance.Neutral" OnClick="@(() => EditAsync(context.Id))" />
</TemplateColumn>
</FluentDataGrid>
@code
{
private RenderFragment template = @<span />;
PaginationState pagination = new PaginationState() { ItemsPerPage = 20 };
}

View file

@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.UI.Components.Pages.Dialogs;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class Accounts : ComponentBase
{
[Inject]
private IAccountRepository accountRepository { get; set; } = null!;
[Inject]
private IDialogService dialogService { get; set; } = null!;
private IQueryable<Account>? accountQueryable;
protected override Task OnInitializedAsync()
{
accountQueryable = accountRepository.GetAccountList().AsQueryable();
return base.OnInitializedAsync();
}
private async Task AddAsync()
{
var data = new Account();
var dialog = await dialogService.ShowDialogAsync<AccountDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Konto",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
accountQueryable = accountRepository.GetAccountList().AsQueryable();
}
}
private async Task EditAsync(string id)
{
var data = accountRepository.GetAccount(id);
var dialog = await dialogService.ShowDialogAsync<AccountDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Konto",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
accountQueryable = accountRepository.GetAccountList().AsQueryable();
}
}
private async Task DeleteAsync(string id)
{
var account = accountRepository.GetAccount(id);
var dialog = await dialogService.ShowConfirmationAsync($"Konto '{account.Name}' wirklich löschen?", "Ja", "Nein", "Konto löschen");
var result = await dialog.Result;
if (!result.Cancelled)
{
accountRepository.DeleteAccount(id);
accountQueryable = accountRepository.GetAccountList().AsQueryable();
}
}
}

View file

@ -0,0 +1,19 @@
@page "/banktransactions"
<PageTitle>BankTransactions</PageTitle>
<FluentPaginator State="@pagination" />
<div id="datagrid-container">
<FluentDataGrid Items="@TransactionList" Pagination="@pagination" RowSize="@rowSize" AutoItemsPerPage="true" Style="overflow-y:hidden;">
<PropertyColumn Title="Buchungsdatum" Width="100" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" />
<PropertyColumn Title="Valutadatum" Width="100" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true" />
<PropertyColumn Title="Text" Property="@(p => p.Text)" />
<PropertyColumn Title="Betrag" Width="100" Property="@(p => p.Value)" Format="C2" Align="Align.End"/>
</FluentDataGrid>
</div>
@code {
DataGridRowSize rowSize = DataGridRowSize.Small;
PaginationState pagination = new PaginationState() { ItemsPerPage = 10 };
}

View file

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class BankTransactions : ComponentBase
{
[Inject]
private IBankTransactionRepository bankTransactionRepository { get; set; } = null!;
private IQueryable<BankTransaction>? TransactionList;
protected override Task OnInitializedAsync()
{
TransactionList = bankTransactionRepository.GetBankTransactionList().AsQueryable();
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,29 @@
#datagrid-container {
height: calc(100% - 3rem);
min-height: 8rem;
overflow-x: auto;
overflow-y: hidden;
}
article {
min-height: 32rem;
max-height: 80dvh;
}
.demo-section-content {
height: calc(100% - 10rem);
}
.demo-section-example {
min-height: 135px !important;
height: 100%;
}
fluent-tabs {
height: 100%;
}
#tab-example-autoitemsperpage-panel {
height: 100% !important;
max-height: calc(100% - 2rem) !important;
}

View file

@ -0,0 +1,26 @@
@page "/bookingrules"
@rendermode InteractiveServer
<PageTitle>Buchungsregeln</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neue Regel hinzufügen</FluentButton>
<FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@bookingRulesQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.LookupText)" Sortable="true" IsDefaultSortColumn="true" />
<PropertyColumn Property="@(p => p.LookupValue)" Format="C2" Align="Align.End"/>
<PropertyColumn Property="@(p => p.BookingText)" />
<PropertyColumn Property="@(p => p.Account)" />
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))" />
</TemplateColumn>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Edit())" Appearance="Appearance.Neutral" OnClick="@(() => EditAsync(context.Id))" />
</TemplateColumn>
</FluentDataGrid>
@code
{
private RenderFragment template = @<span />;
PaginationState pagination = new PaginationState() { ItemsPerPage = 20 };
}

View file

@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.UI.Components.Pages.Dialogs;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class BookingRules : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
[Inject]
private IBookingRuleRepository bookingRuleRepository { get; set; } = null!;
[Inject]
private IDialogService dialogService { get; set; } = null!;
private IQueryable<BookingRuleDataset>? bookingRulesQueryable;
protected override Task OnInitializedAsync()
{
bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable();
return base.OnInitializedAsync();
}
private async Task AddAsync()
{
var data = new BookingRule();
var dialog = await dialogService.ShowDialogAsync<BookingRuleDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Buchungsregel",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable();
}
}
private async Task EditAsync(string id)
{
var data = bookingRuleRepository.GetBookingRule(id);
var dialog = await dialogService.ShowDialogAsync<BookingRuleDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Buchungsregel",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable();
}
}
private async Task DeleteAsync(string id)
{
var bookingRule = bookingRuleRepository.GetBookingRule(id);
var dialog = await dialogService.ShowConfirmationAsync($"Regel '{bookingRule.LookupText}' wirklich löschen?", "Ja", "Nein", "Buchungsregel löschen");
var result = await dialog.Result;
if (!result.Cancelled)
{
bookingRuleRepository.DeleteBookingRule(id);
bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable();
}
}
}

View file

@ -0,0 +1,23 @@
@page "/bookingtexts"
@rendermode InteractiveServer
<PageTitle>Buchungstexte</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neuer Text hinzufügen</FluentButton>
<FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@bookingTextQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.Text)" Sortable="true" IsDefaultSortColumn="true" />
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))" />
</TemplateColumn>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Edit())" Appearance="Appearance.Neutral" OnClick="@(() => EditAsync(context.Id))" />
</TemplateColumn>
</FluentDataGrid>
@code
{
private RenderFragment template = @<span />;
PaginationState pagination = new PaginationState() { ItemsPerPage = 20 };
}

View file

@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.UI.Components.Pages.Dialogs;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class BookingTexts : ComponentBase
{
[Inject]
private IBookingTextRepository bookingTextRepository { get; set; } = null!;
[Inject]
private IDialogService dialogService { get; set; } = null!;
private IQueryable<BookingText>? bookingTextQueryable;
protected override Task OnInitializedAsync()
{
bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable();
return base.OnInitializedAsync();
}
private async Task AddAsync()
{
var data = new BookingText();
var dialog = await dialogService.ShowDialogAsync<BookingTextDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Add Buchungstext",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable();
}
}
private async Task EditAsync(string id)
{
var data = bookingTextRepository.GetBookingText(id);
var dialog = await dialogService.ShowDialogAsync<BookingTextDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Edit Buchungstext",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable();
}
}
private async Task DeleteAsync(string id)
{
var bookingText = bookingTextRepository.GetBookingText(id);
var dialog = await dialogService.ShowConfirmationAsync($"Buchungstext '{bookingText.Text}' wirklich löschen?", "Ja", "Nein", "Buchungstext löschen");
var result = await dialog.Result;
if (!result.Cancelled)
{
bookingTextRepository.DeleteBookingText(id);
bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable();
}
}
}

View file

@ -0,0 +1,6 @@
@page "/Assets"
@rendermode InteractiveServer
<PageTitle>Ausgaben</PageTitle>
<PlotlyChart style="height: 700px; min-height: 500px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />

View file

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Components;
using Plotly.Blazor;
using Plotly.Blazor.Traces;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Charts;
public partial class Assets : ComponentBase
{
private PlotlyChart chart = null!;
private Config config = null!;
private Plotly.Blazor.Layout layout = null!;
private IList<ITrace> data = null!;
[Inject]
private IViewService viewService { get; set; } = null!;
protected override Task OnInitializedAsync()
{
config = new Config
{
Responsive = true
};
layout = new Plotly.Blazor.Layout
{
};
var accounts = viewService.GetAccountViewList().Where(a => a.Class == ClassIds.Activa);
var values = new List<object>();
var labels = new List<object>();
var ids = new List<object>();
foreach (var account in accounts)
{
if (account.BalanceCHF > 0)
{
values.Add(account.BalanceCHF);
labels.Add(account.Name);
ids.Add(account.Id);
}
}
data = new List<ITrace>
{
new Pie
{
Values = values,
Labels = labels,
Ids = ids
}
};
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,6 @@
@page "/spendings"
@rendermode InteractiveServer
<PageTitle>Ausgaben</PageTitle>
<PlotlyChart style="height: 700px; min-height: 500px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />

View file

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Components;
using Plotly.Blazor;
using Plotly.Blazor.Traces;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Charts;
public partial class Spendings : ComponentBase
{
private PlotlyChart chart = null!;
private Config config = null!;
private Plotly.Blazor.Layout layout = null!;
private IList<ITrace> data = null!;
[Inject]
private IViewService viewService { get; set; } = null!;
protected override Task OnInitializedAsync()
{
config = new Config
{
Responsive = true
};
layout = new Plotly.Blazor.Layout
{
};
var accounts = viewService.GetAccountViewList().Where(a => a.Class == ClassIds.Expenses);
var values = new List<object>();
var labels = new List<object>();
foreach (var grp in accounts.GroupBy(a => a.SubClass).Select(a => new {Key = a.Key, List = a.ToList()}))
{
if (grp.List.Sum(g => g.Balance) != 0)
{
values.Add(grp.List.Sum(g => g.Balance));
labels.Add(grp.List.First().SubClassName);
}
}
data = new List<ITrace>
{
new Pie
{
Values =values,
Labels = labels
}
};
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,6 @@
@page "/SpendingsOverTime"
@rendermode InteractiveServer
<PageTitle>Ausgaben im Verlauf</PageTitle>
<PlotlyChart style="height: 700px; min-height: 500px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />

View file

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Components;
using Plotly.Blazor;
using Plotly.Blazor.LayoutLib;
using Plotly.Blazor.Traces;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Charts;
public partial class SpendingsOverTime : ComponentBase
{
private PlotlyChart chart = null!;
private Config config = null!;
private Plotly.Blazor.Layout layout = null!;
private IList<ITrace> data = new List<ITrace>();
[Inject]
private IChartService chartService { get; set; } = null!;
protected override Task OnInitializedAsync()
{
config = new Config
{
Responsive = true
};
layout = new Plotly.Blazor.Layout
{
Title = new Title { Text = "Ausgaben im Verlauf" },
BarMode = BarModeEnum.Stack,
YAxis = new List<YAxis>
{
new()
{
Title = new Plotly.Blazor.LayoutLib.YAxisLib.Title { Text = "CHF" }
}
},
XAxis = new List<XAxis>
{
new()
{
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Date
}
}
};
var dataSeries = chartService.GetExpensesPerMonth();
foreach (var dataSerie in dataSeries)
{
data.Add(new Bar
{
Name = dataSerie.Name,
X = dataSerie.X.Select(d => (object)d).ToList(),
Y = dataSerie.Y.Select(d => (object)d).ToList(),
});
};
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,24 @@
@page "/Classes"
@rendermode InteractiveServer
<PageTitle>Klassen</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neue Klasse hinzufügen</FluentButton>
<FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@subclassQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.Number)" Sortable="true" IsDefaultSortColumn="true" Width="200px" />
<PropertyColumn Property="@(p => p.Name)" Sortable="true" />
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))" />
</TemplateColumn>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Edit())" Appearance="Appearance.Neutral" OnClick="@(() => EditAsync(context.Id))" />
</TemplateColumn>
</FluentDataGrid>
@code
{
private RenderFragment template = @<span />;
PaginationState pagination = new PaginationState() { ItemsPerPage = 20 };
}

View file

@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.UI.Components.Pages.Dialogs;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class Classes : ComponentBase
{
[Inject]
private ISubclassRepository subclassRepository { get; set; } = null!;
[Inject]
private IDialogService dialogService { get; set; } = null!;
private IQueryable<SubClass>? subclassQueryable;
protected override Task OnInitializedAsync()
{
subclassQueryable = subclassRepository.GetSubClassList().AsQueryable();
return base.OnInitializedAsync();
}
private async Task AddAsync()
{
var data = new SubClass();
var dialog = await dialogService.ShowDialogAsync<ClassDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Klasse",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
subclassQueryable = subclassRepository.GetSubClassList().AsQueryable();
}
}
private async Task EditAsync(string id)
{
var data = subclassRepository.GetSubClass(id);
var dialog = await dialogService.ShowDialogAsync<ClassDialog>(data, new DialogParameters()
{
Height = "500px",
Title = "Klasse",
PreventDismissOnOverlayClick = true,
PreventScroll = true,
});
var result = await dialog.Result;
if (!result.Cancelled && result.Data != null)
{
subclassQueryable = subclassRepository.GetSubClassList().AsQueryable();
}
}
private async Task DeleteAsync(string id)
{
var subclass = subclassRepository.GetSubClass(id);
var dialog = await dialogService.ShowConfirmationAsync($"Klasse '{subclass.Name}' wirklich löschen?", "Ja", "Nein", "Klasse löschen");
var result = await dialog.Result;
if (!result.Cancelled)
{
subclassRepository.DeleteSubClass(id);
subclassQueryable = subclassRepository.GetSubClassList().AsQueryable();
}
}
}

View file

@ -0,0 +1,36 @@
@implements IDialogContentComponent<Schaad.Accounting.Models.Account>
@rendermode InteractiveServer
<FluentDialogHeader ShowDismiss="true">
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Regular.Size24.WindowApps())" />
<FluentLabel Typo="Typography.PaneHeader">
@Dialog.Instance.Parameters.Title
</FluentLabel>
</FluentStack>
</FluentDialogHeader>
<FluentDialogBody>
<EditForm EditContext="@editContext">
<DataAnnotationsValidator />
<FluentNumberField @bind-Value="@Content.Number" Label="Nummer" Style="width: 300px;"/>
<FluentTextField @bind-Value="@Content.Name" Label="Name" Style="width: 300px;"/>
<FluentTextField @bind-Value="@Content.Currency" Label="Währung" Style="width: 300px;"/>
<FluentTextField @bind-Value="@Content.BankAccountNumber" Label="Bankkonto" Style="width: 300px;"/>
<FluentNumberField @bind-Value="@Content.StartBalance" Label="Startsaldo" Style="width: 300px;"/>
<div>
<FluentValidationSummary />
</div>
</EditForm>
</FluentDialogBody>
<FluentDialogFooter>
<FluentButton Appearance="Appearance.Neutral"
OnClick="@CancelAsync">
Abbrechen
</FluentButton>
<FluentButton Appearance="Appearance.Accent"
OnClick="@SaveAsync">
Speichern
</FluentButton>
</FluentDialogFooter>

View file

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Dialogs;
public partial class AccountDialog : ComponentBase
{
private EditContext editContext = null!;
[CascadingParameter]
public FluentDialog Dialog { get; set; } = null!;
[Parameter]
public Account Content { get; set; } = null!;
[Inject]
private IAccountRepository accountRepository { get; set; } = null!;
protected override void OnInitialized()
{
editContext = new EditContext(Content);
}
private async Task SaveAsync()
{
if (editContext.Validate())
{
accountRepository.SaveAccount(Content);
await Dialog.CloseAsync(Content);
}
}
private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}

View file

@ -0,0 +1,46 @@
@using Schaad.Accounting.Models
@implements IDialogContentComponent<Schaad.Accounting.Models.BookingRule>
@rendermode InteractiveServer
<FluentDialogHeader ShowDismiss="true">
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Regular.Size24.WindowApps())" />
<FluentLabel Typo="Typography.PaneHeader">
@Dialog.Instance.Parameters.Title
</FluentLabel>
</FluentStack>
</FluentDialogHeader>
<FluentDialogBody>
<EditForm EditContext="@editContext">
<DataAnnotationsValidator />
<FluentTextField @bind-Value="@Content.LookupText" Label="Suchtext" Style="width: 300px;"/>
<FluentNumberField @bind-Value="@Content.LookupValue" Label="Suchbetrag" Style="width: 300px;"/>
<FluentTextField @bind-Value="@Content.BookingText" Label="Buchungstext" Style="width: 300px;" />
<FluentSelect TOption="Account"
Label="Konto"
Items="@accounts"
Width="300px"
Height="250px"
Required="true"
Placeholder="Konto auswählen..."
OptionValue="@(p => p.Id)"
OptionText="@(p => p.Number + " / " + p.Name)"
@bind-Value="@Content.AccountId" />
<div>
<FluentValidationSummary />
</div>
</EditForm>
</FluentDialogBody>
<FluentDialogFooter>
<FluentButton Appearance="Appearance.Neutral"
OnClick="@CancelAsync">
Abbrechen
</FluentButton>
<FluentButton Appearance="Appearance.Accent"
OnClick="@SaveAsync">
Speichern
</FluentButton>
</FluentDialogFooter>

View file

@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Dialogs;
public partial class BookingRuleDialog : ComponentBase
{
private EditContext editContext = null!;
[CascadingParameter]
public FluentDialog Dialog { get; set; } = null!;
[Parameter]
public BookingRule Content { get; set; } = null!;
[Inject]
private IBookingRuleRepository bookingRuleRepository { get; set; } = null!;
[Inject]
private IViewService viewService { get; set; } = null!;
private IReadOnlyList<Account> accounts = [];
protected override void OnInitialized()
{
editContext = new EditContext(Content);
accounts = viewService.GetAccountViewList();
}
private async Task SaveAsync()
{
if (editContext.Validate())
{
bookingRuleRepository.SaveBookingRule(Content);
await Dialog.CloseAsync(Content);
}
}
private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}

View file

@ -0,0 +1,32 @@
@implements IDialogContentComponent<Schaad.Accounting.Models.BookingText>
@rendermode InteractiveServer
<FluentDialogHeader ShowDismiss="true">
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Regular.Size24.WindowApps())" />
<FluentLabel Typo="Typography.PaneHeader">
@Dialog.Instance.Parameters.Title
</FluentLabel>
</FluentStack>
</FluentDialogHeader>
<FluentDialogBody>
<EditForm EditContext="@editContext">
<DataAnnotationsValidator />
<FluentTextField @bind-Value="@Content.Text" Label="Buchungstext" />
<div>
<FluentValidationSummary />
</div>
</EditForm>
</FluentDialogBody>
<FluentDialogFooter>
<FluentButton Appearance="Appearance.Neutral"
OnClick="@CancelAsync">
Abbrechen
</FluentButton>
<FluentButton Appearance="Appearance.Accent"
OnClick="@SaveAsync">
Speichern
</FluentButton>
</FluentDialogFooter>

View file

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Dialogs;
public partial class BookingTextDialog : ComponentBase
{
private EditContext editContext = null!;
[CascadingParameter]
public FluentDialog Dialog { get; set; } = null!;
[Parameter]
public BookingText Content { get; set; } = null!;
[Inject]
private IBookingTextRepository bookingTextRepository { get; set; } = null!;
protected override void OnInitialized()
{
editContext = new EditContext(Content);
}
private async Task SaveAsync()
{
if (editContext.Validate())
{
bookingTextRepository.SaveBookingText(Content);
await Dialog.CloseAsync(Content);
}
}
private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}

View file

@ -0,0 +1,33 @@
@implements IDialogContentComponent<Schaad.Accounting.Models.SubClass>
@rendermode InteractiveServer
<FluentDialogHeader ShowDismiss="true">
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Regular.Size24.WindowApps())" />
<FluentLabel Typo="Typography.PaneHeader">
@Dialog.Instance.Parameters.Title
</FluentLabel>
</FluentStack>
</FluentDialogHeader>
<FluentDialogBody>
<EditForm EditContext="@editContext">
<DataAnnotationsValidator />
<FluentNumberField @bind-Value="@Content.Number" Label="Nummer" Style="width: 300px;"/>
<FluentTextField @bind-Value="@Content.Name" Label="Name" Style="width: 300px;"/>
<div>
<FluentValidationSummary />
</div>
</EditForm>
</FluentDialogBody>
<FluentDialogFooter>
<FluentButton Appearance="Appearance.Neutral"
OnClick="@CancelAsync">
Abbrechen
</FluentButton>
<FluentButton Appearance="Appearance.Accent"
OnClick="@SaveAsync">
Speichern
</FluentButton>
</FluentDialogFooter>

View file

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages.Dialogs;
public partial class ClassDialog : ComponentBase
{
private EditContext editContext = null!;
[CascadingParameter]
public FluentDialog Dialog { get; set; } = null!;
[Parameter]
public SubClass Content { get; set; } = null!;
[Inject]
private ISubclassRepository bookingRuleRepository { get; set; } = null!;
protected override void OnInitialized()
{
editContext = new EditContext(Content);
}
private async Task SaveAsync()
{
if (editContext.Validate())
{
bookingRuleRepository.SaveSubClass(Content);
await Dialog.CloseAsync(Content);
}
}
private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}

View file

@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View file

@ -0,0 +1,7 @@
@page "/"
<h3>Home</h3>
@foreach (var bankTransaction in bankTransactions)
{
<p>@bankTransaction.BankAccountNumber</p>
}

View file

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class Home : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
private List<BankTransaction> bankTransactions = null!;
protected override Task OnInitializedAsync()
{
bankTransactions = viewService.GetOpenBankTransactionList();
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,99 @@
@page "/BalanceReport"
<h3>@header</h3>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th class="right">Aktiven</th>
<th class="right">Passiven</th>
</tr>
<tr>
<th colspan="4"><hr /></th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4" class="header">Aktiven</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach (var account in balance.ActivaAccountList.OrderBy(m => m.Number))
{
@if (account.BalanceCHF == 0) { continue; }
<tr>
<td>@account.Number</td>
<td>@account.Name</td>
<td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td>
<td></td>
</tr>
}
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="2" class="total">Total Aktiven</td>
<td class="right total">CHF @balance.TotalActivaCHF.ToFormattedString()</td>
<td></td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="4" class="header">Passiven</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach (var account in balance.PassivaAccountList.OrderBy(m => m.Number))
{
@if (account.BalanceCHF == 0) { continue; }
<tr>
<td>@account.Number</td>
<td>@account.Name</td>
<td></td>
<td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td>
</tr>
}
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="2" class="total">Total Passiven</td>
<td></td>
<td class="right total">@balance.TotalPassivaCHF.ToFormattedString()</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="2" class="total">Total</td>
<td class="right">@balance.TotalActivaCHF.ToFormattedString()</td>
<td class="right">@balance.TotalPassivaCHF.ToFormattedString()</td>
</tr>
<tr>
<td colspan="2" class="total">Eigenkapital</td>
<td class="right sum"></td>
<td class="right sum">@win.ToFormattedString()</td>
</tr>
<tr>
<td colspan="2" class="total">Bilanzsumme</td>
<td class="right total sum">CHF @balance.TotalActivaCHF.ToFormattedString()</td>
<td class="right total sum">CHF @((balance.TotalPassivaCHF + win).ToFormattedString())</td>
</tr>
<tr>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Datasets.Reports;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Reports;
public partial class BalanceReport : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
private BalanceDataset balance = null!;
private decimal win;
private string header = null!;
private string footer = null!;
protected override Task OnInitializedAsync()
{
balance = viewService.GetBalanceView();
win = balance.TotalActivaCHF - balance.TotalPassivaCHF;
(header, footer) = Report.GetViewDataTitleAndFooter("Bilanz", settingsService);
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,76 @@
@page "/BalanceSheetReport"
<h3>@header</h3>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report">
<tbody>
<tr>
<td colspan="3" class="header">Einnahmen</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach (var account in balanceSheet.ActivaAccountList.OrderBy(m => m.Number))
{
@if (account.StartBalanceCHF == 0) { continue; }
<tr>
<td>@account.Number</td>
<td>Vortrag von @(balanceSheet.Year - 1) / @account.Name</td>
<td class="right">@Math.Abs(account.StartBalanceCHF).ToFormattedString()</td>
</tr>
}
@foreach (var account in balanceSheet.IncomeAccountList.OrderBy(m => m.Number))
{
@if (account.BalanceCHF == 0) { continue; }
<tr>
<td>@account.Number</td>
<td>@account.Name</td>
<td class="right">@Math.Abs(account.BalanceCHF).ToFormattedString()</td>
</tr>
}
<tr>
<td colspan="3"><hr /></td>
</tr>
<tr>
<td colspan="3" class="header">Ausgaben</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach (var account in balanceSheet.ExpensesAccountList.OrderBy(m => m.Number))
{
@if (account.BalanceCHF == 0) { continue; }
<tr>
<td>@account.Number</td>
<td>@account.Name</td>
<td class="right">@account.BalanceCHF.ToFormattedString()</td>
</tr>
}
<tr>
<td colspan="3"><hr /></td>
</tr>
<tr>
<td colspan="2">Total Einnahmen</td>
<td class="right">@balanceSheet.ProfitCHF.ToFormattedString()</td>
</tr>
<tr>
<td colspan="2">Total Ausgaben</td>
<td class="right sum">-@balanceSheet.LossCHF.ToFormattedString()</td>
</tr>
<tr>
<td colspan="2" class="total">Übertrag @(balanceSheet.Year + 1)</td>
<td class="right total sum">CHF @win.ToFormattedString()</td>
</tr>
<tr>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Datasets.Reports;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Reports;
public partial class BalanceSheetReport : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
private BalanceSheetDataset balanceSheet = null!;
private decimal win;
private string header = null!;
private string footer = null!;
protected override Task OnInitializedAsync()
{
balanceSheet = viewService.GetBalanceSheetView(settingsService.GetYear());
win = balanceSheet.ProfitCHF - balanceSheet.LossCHF;
(header, footer) = Report.GetViewDataTitleAndFooter("Jahresabschluss", settingsService);
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,112 @@
@page "/DetailReport"
<h3>@header</h3>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th class="right">Betrag</th>
</tr>
<tr>
<th colspan="3"><hr /></th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="3" class="header">Einkünfte</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 3).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr>
<td class="header">@account.Number</td>
<td class="header">@account.Name</td>
<td class="header right">@Math.Abs(account.Balance).ToFormattedString()</td>
</tr>
@foreach(var trx in transactions.Where(t => t.TargetAccountId == account.Id || t.OriginAccountId == account.Id).OrderBy(t => t.ValueDate))
{
<tr>
<td>@trx.ValueDate.ToString("dd.MM.yyyy")</td>
<td>@trx.Text</td>
<td class="right">
@if (trx.OriginAccountId == account.Id)
{
@trx.Value.ToFormattedString();
}
else
{
<span>-</span>@trx.Value.ToFormattedString();
}
</td>
</tr>
}
<tr>
<td colspan="3"><hr /></td>
</tr>
}
<tr>
<td colspan="2" class="total right">Total Einkünfte</td>
<td class="right total">@profit.ToFormattedString()</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
<tr>
<td colspan="3" class="header">Ausgaben</td>
</tr>
<tr>
<td colspan="3"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 4).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr>
<td class="header">@account.Number</td>
<td class="header">@account.Name</td>
<td class="header right">@account.Balance.ToFormattedString()</td>
</tr>
@foreach(var trx in transactions.Where(t => t.TargetAccountId == account.Id || t.OriginAccountId == account.Id).OrderBy(t => t.ValueDate))
{
<tr>
<td>@trx.ValueDate.ToString("dd.MM.yyyy")</td>
<td>@trx.Text</td>
<td class="right">
@if (trx.TargetAccountId == account.Id)
{
@trx.Value.ToFormattedString();
}
else
{
<span>-</span>@trx.Value.ToFormattedString();
}
</td>
</tr>
}
<tr>
<td colspan="3"><hr /></td>
</tr>
}
<tr>
<td colspan="2" class="total right">Total Ausgaben</td>
<td class="right total">@loss.ToFormattedString()</td>
</tr>
<tr>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Reports;
public partial class DetailReport : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
private IReadOnlyList<AccountDataset> accounts = null!;
private IReadOnlyList<TransactionDataset> transactions = null!;
private decimal profit;
private decimal loss;
private string header = null!;
private string footer = null!;
protected override Task OnInitializedAsync()
{
accounts = viewService.GetAccountViewList();
transactions = viewService.GetTransactionViewList();
profit = Math.Abs(accounts.Where(m => m.Class == 3).Sum(m => m.Balance));
loss = Math.Abs(accounts.Where(m => m.Class == 4).Sum(m => m.Balance));
(header, footer) = Report.GetViewDataTitleAndFooter("Detailaufstellung", settingsService);
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,99 @@
@page "/ProfitLossReport"
<h3>@header</h3>
<a href="javascript:window.print()" class="hidePrint">Print</a>
<br />
<br />
<table class="report">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th class="right">Aufwand</th>
<th class="right">Ertrag</th>
</tr>
<tr>
<th colspan="4"><hr /></th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4" class="header">Einkünfte</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 3).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr>
<td>@account.Number</td>
<td>@account.Name</td>
<td></td>
<td class="right">@Math.Abs(account.Balance).ToFormattedString()</td>
</tr>
}
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="2" class="total">Total Einkünfte</td>
<td></td>
<td class="right total">@profit.ToFormattedString()</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="4" class="header">Ausgaben</td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
@foreach(var account in accounts.Where(m => m.Class == 4).OrderBy(m => m.Number))
{
@if (account.Balance == 0) {continue;}
<tr>
<td>@account.Number</td>
<td>@account.Name</td>
<td class="right">@Math.Abs(account.Balance).ToFormattedString()</td>
<td></td>
</tr>
}
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="2" class="total">Total Ausgaben</td>
<td class="right total">@loss.ToFormattedString()</td>
<td></td>
</tr>
<tr>
<td colspan="4"><hr /></td>
</tr>
<tr>
<td colspan="2" class="total"></td>
<td class="right total">@loss.ToFormattedString()</td>
<td class="right total">@profit.ToFormattedString()</td>
</tr>
<tr>
<td colspan="2" class="total">Gewinn/Verlust</td>
<td class="right total sum">@win.ToFormattedString()</td>
<td class="right total sum"></td>
</tr>
<tr>
<td colspan="2" class="total">Total</td>
<td class="right total sum">@profit.ToFormattedString()</td>
<td class="right total sum">@profit.ToFormattedString()</td>
</tr>
<tr>
<td colspan="4" style="height: 30px">&nbsp;</td>
</tr>
<tr>
<td colspan="4" class="footer">@footer</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Reports;
public partial class ProfitLossReport : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
private IReadOnlyList<AccountDataset> accounts = null!;
private decimal profit;
private decimal loss;
private decimal win;
private string header = null!;
private string footer = null!;
protected override Task OnInitializedAsync()
{
accounts = viewService.GetAccountViewList();
profit = Math.Abs(accounts.Where(m => m.Class == 3).Sum(m => m.Balance));
loss = Math.Abs(accounts.Where(m => m.Class == 4).Sum(m => m.Balance));
win = profit-loss;
(header, footer) = Report.GetViewDataTitleAndFooter("Erfolgsrechnung", settingsService);
return base.OnInitializedAsync();
}
}

View file

@ -0,0 +1,23 @@
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages.Reports;
public static class Report
{
public static (string header, string footer) GetViewDataTitleAndFooter(string title, ISettingsService settingsService)
{
var header = $"{title} {settingsService.GetMandator()} {settingsService.GetYear()}";
var footer = "Stand: ";
if (DateTime.Now.Year == settingsService.GetYear())
{
footer += DateTime.Now.ToString("dd.MM.yyyy");
}
else
{
footer += "31.12." + settingsService.GetYear();
}
return (header, footer);
}
}

View file

@ -0,0 +1,53 @@
@page "/transactions"
@rendermode InteractiveServer
<PageTitle>Transaktionen</PageTitle>
<FluentStack Orientation="Orientation.Horizontal" Width="100%">
<FluentNavMenu Width="700">
@foreach (var gr in accounts.GroupBy(a => a.ClassName))
{
<FluentNavGroup Title="@(gr.Key)" Expanded="true">
@foreach (var account in gr)
{
<FluentNavLink OnClick="() => ShowTransactions(account.Id)" Icon="@(new Icons.Regular.Size24.CalendarAgenda())">
<FluentStack Orientation="Orientation.Horizontal">
<div>
@account.Number / @account.Name
</div>
<div style="margin-left: auto;">
<FluentBadge Appearance="Appearance.Neutral">
@account.BalanceCHF.ToString("C2")
</FluentBadge>
</div>
</FluentStack>
</FluentNavLink>
}
</FluentNavGroup>
}
</FluentNavMenu>
<FluentBodyContent>
<FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@transactionList" Pagination="@pagination">
<PropertyColumn Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" />
<PropertyColumn Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true" />
<PropertyColumn Property="@(p => p.Text)" />
<TemplateColumn Title="Gegenkonto">
@GetAccountName(context)
</TemplateColumn>
<PropertyColumn Property="@(p => p.Value)" Format="C2" Align="Align.End"/>
</FluentDataGrid>
</FluentBodyContent>
</FluentStack>
@code
{
private RenderFragment template = @<span />;
PaginationState pagination = new PaginationState() { ItemsPerPage = 20 };
}

View file

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Components;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.UI.Components.Pages;
public partial class Transactions : ComponentBase
{
[Inject]
private IViewService viewService { get; set; } = null!;
private IReadOnlyList<AccountDataset> accounts = null!;
private IQueryable<TransactionDataset>? transactionList;
private string? selectedAccountId;
protected override Task OnInitializedAsync()
{
accounts = viewService.GetAccountViewList();
return base.OnInitializedAsync();
}
private void ShowTransactions(string accountId)
{
selectedAccountId = accountId;
transactionList = viewService.GetTransactionViewList(accountId).AsQueryable();
}
private string GetAccountName(TransactionDataset transaction)
{
if (selectedAccountId == transaction.OriginAccountId)
{
return transaction.TargetAccount.Name;
}
return transaction.OriginAccount.Name;
}
}

View file

@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View file

@ -0,0 +1,16 @@
@using System.ComponentModel.DataAnnotations
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.FluentUI.AspNetCore.Components
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
@using Microsoft.JSInterop
@using Plotly.Blazor
@using Schaad.Accounting.UI
@using Schaad.Accounting.UI.Components
@using Schaad.Accounting.Interfaces

Some files were not shown because too many files have changed in this diff Show more