diff --git a/ucalc/Data/Billing.cs b/ucalc/Data/Billing.cs index bd9a0c9..d7604bb 100644 --- a/ucalc/Data/Billing.cs +++ b/ucalc/Data/Billing.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; #pragma warning disable 659 @@ -43,9 +45,17 @@ namespace UCalc.Data public class Address { + [JsonProperty(PropertyName = "street"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Street { get; set; } + + [JsonProperty(PropertyName = "houseNumber"), JsonRequired, + JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string HouseNumber { get; set; } + + [JsonProperty(PropertyName = "city"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string City { get; set; } + + [JsonProperty(PropertyName = "postCode"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Postcode { get; set; } public Address() @@ -83,8 +93,13 @@ namespace UCalc.Data public class BankAccount { + [JsonProperty(PropertyName = "iban"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Iban { get; set; } + + [JsonProperty(PropertyName = "bic"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Bic { get; set; } + + [JsonProperty(PropertyName = "bankName"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string BankName { get; set; } public BankAccount() @@ -119,8 +134,13 @@ namespace UCalc.Data public class Flat { - public Guid Id { get; private set; } + [JsonProperty(PropertyName = "id"), JsonRequired] + public Guid Id { get; set; } + + [JsonProperty(PropertyName = "name"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Name { get; set; } + + [JsonProperty(PropertyName = "size"), JsonRequired] public int Size { get; set; } public Flat() @@ -161,16 +181,37 @@ namespace UCalc.Data public class Tenant { - public Guid Id { get; private set; } + [JsonProperty(PropertyName = "id"), JsonRequired] + public Guid Id { get; set; } + + [JsonProperty(PropertyName = "salutation"), JsonRequired, JsonConverter(typeof(StringEnumConverter))] public Salutation Salutation { get; set; } + + [JsonProperty(PropertyName = "name"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Name { get; set; } + + [JsonProperty(PropertyName = "personCount"), JsonRequired] public int PersonCount { get; set; } - public BankAccount BankAccount { get; private set; } + + [JsonProperty(PropertyName = "bankAccount"), JsonRequired] + public BankAccount BankAccount { get; set; } + + [JsonProperty(PropertyName = "entryDate")] public DateTime? EntryDate { get; set; } + + [JsonProperty(PropertyName = "departureDate")] public DateTime? DepartureDate { get; set; } - public HashSet RentedFlats { get; private set; } + + [JsonProperty(PropertyName = "rentedFlats"), JsonRequired, JsonConverter(typeof(FlatSerializationConverter))] + public HashSet RentedFlats { get; set; } + + [JsonProperty(PropertyName = "paidRent"), JsonRequired] public decimal PaidRent { get; set; } + + [JsonProperty(PropertyName = "customMessage1"), JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string CustomMessage1 { get; set; } + + [JsonProperty(PropertyName = "customMessage2"), JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string CustomMessage2 { get; set; } public Tenant() @@ -179,6 +220,8 @@ namespace UCalc.Data Name = ""; BankAccount = new BankAccount(); RentedFlats = new HashSet(); + CustomMessage1 = ""; + CustomMessage2 = ""; } private bool Equals(Tenant other) @@ -218,12 +261,24 @@ namespace UCalc.Data public class Landlord { + [JsonProperty(PropertyName = "salutation"), JsonRequired, JsonConverter(typeof(StringEnumConverter))] public Salutation Salutation { get; set; } + + [JsonProperty(PropertyName = "name"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Name { get; set; } + + [JsonProperty(PropertyName = "mailAddress"), JsonRequired, + JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string MailAddress { get; set; } + + [JsonProperty(PropertyName = "phone"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Phone { get; set; } - public Address Address { get; private set; } - public BankAccount BankAccount { get; private set; } + + [JsonProperty(PropertyName = "address"), JsonRequired] + public Address Address { get; set; } + + [JsonProperty(PropertyName = "bankAccount"), JsonRequired] + public BankAccount BankAccount { get; set; } public Landlord() { @@ -263,8 +318,11 @@ namespace UCalc.Data public class House { - public Address Address { get; private set; } - public List Flats { get; private set; } + [JsonProperty(PropertyName = "address"), JsonRequired] + public Address Address { get; set; } + + [JsonProperty(PropertyName = "flats"), JsonRequired] + public List Flats { get; set; } public House() { @@ -296,9 +354,14 @@ namespace UCalc.Data public class CostEntryDetails { + [JsonProperty(PropertyName = "totalAmount"), JsonRequired] public decimal TotalAmount { get; set; } + + [JsonProperty(PropertyName = "unitCount"), JsonRequired] public decimal UnitCount { get; set; } - public List DiscountsInUnits { get; private set; } + + [JsonProperty(PropertyName = "discountsInUnits"), JsonRequired] + public List DiscountsInUnits { get; set; } public CostEntryDetails() { @@ -331,9 +394,16 @@ namespace UCalc.Data public class CostEntry { + [JsonProperty(PropertyName = "startDate"), JsonRequired] public DateTime StartDate { get; set; } + + [JsonProperty(PropertyName = "endDate"), JsonRequired] public DateTime EndDate { get; set; } + + [JsonProperty(PropertyName = "amount"), JsonRequired] public decimal Amount { get; set; } + + [JsonProperty(PropertyName = "details"), JsonRequired] public CostEntryDetails Details { get; set; } public CostEntry() @@ -393,12 +463,25 @@ namespace UCalc.Data public class Cost { + [JsonProperty(PropertyName = "name"), JsonRequired, JsonConverter(typeof(JsonNullToEmptyStringConverter))] public string Name { get; set; } + + [JsonProperty(PropertyName = "division"), JsonRequired, JsonConverter(typeof(StringEnumConverter))] public CostDivision Division { get; set; } + + [JsonProperty(PropertyName = "affectsAll"), JsonRequired] public bool AffectsAll { get; set; } + + [JsonProperty(PropertyName = "includeUnrented"), JsonRequired] public bool IncludeUnrented { get; set; } - public HashSet AffectedFlats { get; private set; } - public List Entries { get; private set; } + + [JsonProperty(PropertyName = "affectedFlats"), JsonRequired, JsonConverter(typeof(FlatSerializationConverter))] + public HashSet AffectedFlats { get; set; } + + [JsonProperty(PropertyName = "entries"), JsonRequired] + public List Entries { get; set; } + + [JsonProperty(PropertyName = "displayInBill"), JsonRequired] public bool DisplayInBill { get; set; } public Cost() @@ -439,12 +522,23 @@ namespace UCalc.Data public class Billing { + [JsonProperty(PropertyName = "startDate"), JsonRequired] public DateTime StartDate { get; set; } + + [JsonProperty(PropertyName = "endDate"), JsonRequired] public DateTime EndDate { get; set; } - public Landlord Landlord { get; private set; } - public House House { get; private set; } - public List Tenants { get; private set; } - public List Costs { get; private set; } + + [JsonProperty(PropertyName = "landlord"), JsonRequired] + public Landlord Landlord { get; set; } + + [JsonProperty(PropertyName = "house"), JsonRequired] + public House House { get; set; } + + [JsonProperty(PropertyName = "tenants"), JsonRequired] + public List Tenants { get; set; } + + [JsonProperty(PropertyName = "costs"), JsonRequired] + public List Costs { get; set; } public Billing() { diff --git a/ucalc/Data/BillingLoader.cs b/ucalc/Data/BillingLoader.cs index 3bf9595..f1b18d2 100644 --- a/ucalc/Data/BillingLoader.cs +++ b/ucalc/Data/BillingLoader.cs @@ -1,18 +1,129 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace UCalc.Data { + internal class FlatSerializationConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var set = (HashSet) value; + + writer.WriteStartArray(); + + foreach (var flat in set) + { + writer.WriteValue(flat.Id.ToString()); + } + + writer.WriteEndArray(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartArray) + { + throw new JsonException("Expected ["); + } + + while (reader.Read() && reader.TokenType == JsonToken.String) + { + } + + if (reader.TokenType != JsonToken.EndArray) + { + throw new JsonException("Expected ]"); + } + + return new HashSet(); + } + + public override bool CanConvert(Type objectType) + { + throw new InvalidOperationException(); + } + } + + internal class JsonNullToEmptyStringConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return true; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + return existingValue ?? string.Empty; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value ?? string.Empty); + } + } + public class BillingLoader { + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] public Billing Load(string path) { - // TODO - return new Billing(); + // TODO: Support older format + + var content = File.ReadAllText(path); + try + { + var billing = JsonConvert.DeserializeObject(content); + + var idToFlat = new Dictionary(); + foreach (var flat in billing.House.Flats) + { + idToFlat.Add(flat.Id.ToString(), flat); + } + + var root = JObject.Parse(content); + var i = 0; + foreach (var tenantItem in root["tenants"]) + { + foreach (var rentedFlatId in tenantItem["rentedFlats"]) + { + billing.Tenants[i].RentedFlats.Add(idToFlat[rentedFlatId.Value()]); + } + + ++i; + } + + i = 0; + foreach (var tenantItem in root["costs"]) + { + foreach (var rentedFlatId in tenantItem["affectedFlats"]) + { + billing.Costs[i].AffectedFlats.Add(idToFlat[rentedFlatId.Value()]); + } + + ++i; + } + + return billing; + } + catch (JsonException e) + { + throw new IOException($"Could not load JSON with error:\n{e.Message}", e); + } + catch (InvalidCastException e) + { + throw new IOException($"Could not load JSON with error:\n{e.Message}", e); + } } public void Store(string path, Billing billing) { - throw new NotImplementedException(); + File.WriteAllText(path, JsonConvert.SerializeObject(billing, Formatting.Indented)); } } } \ No newline at end of file diff --git a/ucalc/MainWindow.xaml.cs b/ucalc/MainWindow.xaml.cs index 9a3163a..b88c5e0 100644 --- a/ucalc/MainWindow.xaml.cs +++ b/ucalc/MainWindow.xaml.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; @@ -78,9 +77,10 @@ namespace UCalc new BillingWindow(path, billing).Show(); Hide(); } - catch (IOException) + catch (IOException e) { - throw new NotImplementedException(); + MessageBox.Show($"Beim Laden der Datei \"{path}\" ist ein Fehler aufgetreten!\n\nDetails: {e.Message}", + "Fehler!", MessageBoxButton.OK, MessageBoxImage.Error); } } } diff --git a/ucalc/Models/FlatsProperty.cs b/ucalc/Models/FlatsProperty.cs index d3d39e8..6216d22 100644 --- a/ucalc/Models/FlatsProperty.cs +++ b/ucalc/Models/FlatsProperty.cs @@ -51,7 +51,10 @@ namespace UCalc.Models tenant.RentedFlats.Remove(flat); } - // TODO: Revalidate all flats + revalidate tenant rent lists + costs flat lists + foreach (var cost in Model.Root.Costs) + { + cost.AffectedFlats.Remove(flat); + } } } } \ No newline at end of file diff --git a/ucalc/Models/Model.cs b/ucalc/Models/Model.cs index 0349be4..371ad03 100644 --- a/ucalc/Models/Model.cs +++ b/ucalc/Models/Model.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.CompilerServices; using UCalc.Annotations; using UCalc.Data; @@ -48,7 +49,7 @@ namespace UCalc.Models return; } - if (_validated.Add(property)) + if (_validated.Add(property) && !_model._loading) { property.Validate(); } @@ -106,6 +107,18 @@ namespace UCalc.Models return -1; } + var d1 = prop1.DepthInTree(); + var d2 = prop2.DepthInTree(); + if (d1 < d2) + { + return 1; + } + + if (d1 > d2) + { + return -1; + } + return 0; }); @@ -127,6 +140,7 @@ namespace UCalc.Models } private readonly Validator _validator; + private readonly bool _loading; public DateTime StartDate { get; } public DateTime EndDate { get; } public BillingProperty Root { get; } @@ -137,8 +151,12 @@ namespace UCalc.Models StartDate = billing.StartDate; EndDate = billing.EndDate; - using var validator = BeginValidation(); + _loading = true; Root = new BillingProperty(this, null, billing); + _loading = false; + + using var validator = BeginValidation(); + validator.Validate(Root); } public Validator BeginValidation() @@ -149,7 +167,83 @@ namespace UCalc.Models public Billing Dump() { - throw new NotImplementedException(); + var billing = new Billing + { + StartDate = StartDate, + EndDate = EndDate, + Landlord = + { + Salutation = (Salutation) Root.Landlord.Salutation.Value, + Name = Root.Landlord.Name.Value, + MailAddress = Root.Landlord.MailAddress.Value, + Phone = Root.Landlord.Phone.Value, + Address = + { + Street = Root.Landlord.Address.Street.Value, + HouseNumber = Root.Landlord.Address.HouseNumber.Value, + City = Root.Landlord.Address.City.Value, + Postcode = Root.Landlord.Address.Postcode.Value + }, + BankAccount = + { + Iban = Root.Landlord.BankAccount.Iban.Value, + Bic = Root.Landlord.BankAccount.Bic.Value, + BankName = Root.Landlord.BankAccount.BankName.Value + } + }, + House = + { + Address = + { + Street = Root.House.Address.Street.Value, + HouseNumber = Root.House.Address.HouseNumber.Value, + City = Root.House.Address.City.Value, + Postcode = Root.House.Address.Postcode.Value + }, + Flats = Root.House.Flats.Select(flat => new Flat + {Name = flat.Name.Value, Size = int.TryParse(flat.Size.Value, out var n) ? n : 0}).ToList() + } + }; + + var flatPropertyToFlat = new Dictionary(); + for (var i = 0; i < Root.House.Flats.Count; ++i) + { + flatPropertyToFlat.Add(Root.House.Flats[i], billing.House.Flats[i]); + } + + billing.Tenants = Root.Tenants.Select(tenant => new Tenant + { + Salutation = (Salutation) tenant.Salutation.Value, + Name = tenant.Name.Value, + PersonCount = int.TryParse(tenant.PersonCount.Value, out var n) ? n : 0, + BankAccount = + { + Iban = tenant.BankAccount.Iban.Value, + Bic = tenant.BankAccount.Bic.Value, + BankName = tenant.BankAccount.BankName.Value + }, + EntryDate = tenant.EntryDate.Value, + DepartureDate = tenant.DepartureDate.Value, + RentedFlats = + new HashSet(tenant.RentedFlats.Select(rentedFlat => flatPropertyToFlat[rentedFlat])), + PaidRent = decimal.TryParse(tenant.PaidRent.Value, out var d) ? d : 0, + CustomMessage1 = tenant.CustomMessage1.Value, + CustomMessage2 = tenant.CustomMessage2.Value + }).ToList(); + + billing.Costs = Root.Costs.Select(cost => new Cost + { + Name = cost.Name.Value, + Division = (CostDivision) cost.Division.Value, + AffectsAll = cost.AffectsAll.Value, + IncludeUnrented = cost.IncludeUnrented.Value, + AffectedFlats = + new HashSet(cost.AffectedFlats.Select(rentedFlat => flatPropertyToFlat[rentedFlat])), + // TODO: Entries = {}, + DisplayInBill = cost.DisplayInBill.Value + }).ToList(); + + return billing; } public void ResetModified() diff --git a/ucalc/Models/Properties.cs b/ucalc/Models/Properties.cs index 837434c..89106e4 100644 --- a/ucalc/Models/Properties.cs +++ b/ucalc/Models/Properties.cs @@ -67,11 +67,25 @@ namespace UCalc.Models return false; } + + public int DepthInTree() + { + var i = 0; + var property = Parent; + + while (property != null) + { + ++i; + property = property.Parent; + } + + return i; + } } public abstract class ValueProperty : Property { - protected string Name; + protected readonly string Name; private T _value; private readonly List _errors; diff --git a/ucalc/Models/TenantProperty.cs b/ucalc/Models/TenantProperty.cs index 224c71a..1dbedcb 100644 --- a/ucalc/Models/TenantProperty.cs +++ b/ucalc/Models/TenantProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using UCalc.Controls; using UCalc.Data; @@ -48,7 +49,7 @@ namespace UCalc.Models } } - public class RentedFlatsProperty : Property + public class RentedFlatsProperty : Property, IReadOnlyCollection { private const string NoFlatsError = "Gemietete Wohnungen: Es wurde keine Wohnung zugewiesen."; private readonly HashSet _flatProperties; @@ -65,7 +66,8 @@ namespace UCalc.Models _flatProperties.Add(flatToProperty?[flat] ?? throw new InvalidOperationException()); } - Validate(); + using var validator = Model.BeginValidation(); + validator.Validate(this); } public override IReadOnlyList Errors => _errors; @@ -175,6 +177,18 @@ namespace UCalc.Models return thisStart.Intersects(thisEnd, otherStart, otherEnd); } + + public IEnumerator GetEnumerator() + { + return _flatProperties.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int Count => _flatProperties.Count; } public class TenantProperty : NestedProperty diff --git a/ucalc/NewWindow.xaml b/ucalc/NewWindow.xaml index 72a3f5f..05ac936 100644 --- a/ucalc/NewWindow.xaml +++ b/ucalc/NewWindow.xaml @@ -13,8 +13,6 @@ PreviewMouseUp="OnPreviewMouseUp" Icon="logo.ico"> - -