diff --git a/ucalc/Constants.cs b/ucalc/Constants.cs index db656b2..d850bf3 100644 --- a/ucalc/Constants.cs +++ b/ucalc/Constants.cs @@ -16,5 +16,8 @@ namespace UCalc public static readonly ImmutableList SalutationStrs = ((Salutation[]) Enum.GetValues(typeof(Salutation))).Select(value => value.AsString()).ToImmutableList(); + + public static readonly ImmutableList CostDivisionStrs = + ((CostDivision[]) Enum.GetValues(typeof(CostDivision))).Select(value => value.AsString()).ToImmutableList(); } } \ No newline at end of file diff --git a/ucalc/Controls/Converters.cs b/ucalc/Controls/Converters.cs index a03fc1b..80cae75 100644 --- a/ucalc/Controls/Converters.cs +++ b/ucalc/Controls/Converters.cs @@ -79,6 +79,21 @@ namespace UCalc.Controls } } + public class FlatToAffectedConverter : IValueConverter + { + public CostProperty Cost { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return Cost.AffectedFlats.Contains((FlatProperty) value); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new InvalidOperationException(); + } + } + public class EmptyMultiPropertyToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) @@ -133,4 +148,17 @@ namespace UCalc.Controls throw new InvalidOperationException(); } } + + public class NegateConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool?) value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool?) value; + } + } } \ No newline at end of file diff --git a/ucalc/CostWindow.xaml b/ucalc/CostWindow.xaml index 2ba7d02..f351449 100644 --- a/ucalc/CostWindow.xaml +++ b/ucalc/CostWindow.xaml @@ -14,7 +14,14 @@ WindowStartupLocation="CenterOwner" ShowInTaskbar="False" Icon="logo.ico"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + \ No newline at end of file diff --git a/ucalc/CostWindow.xaml.cs b/ucalc/CostWindow.xaml.cs index faa7c12..402ac07 100644 --- a/ucalc/CostWindow.xaml.cs +++ b/ucalc/CostWindow.xaml.cs @@ -1,4 +1,6 @@ using System.Windows; +using System.Windows.Controls; +using UCalc.Controls; using UCalc.Models; namespace UCalc @@ -14,13 +16,40 @@ namespace UCalc Model = model; Cost = cost; House = house; - InitializeComponent(); + + ((FlatToAffectedConverter) FindResource("FlatToAffectedConverter")).Cost = Cost; } private void OnOkClick(object sender, RoutedEventArgs e) { Close(); } + + private void OnFlatChecked(object sender, RoutedEventArgs e) + { + var flat = (FlatProperty) ((CheckBox) sender).DataContext; + + Cost.AffectedFlats.Add(flat); + } + + private void OnFlatUnchecked(object sender, RoutedEventArgs e) + { + var flat = (FlatProperty) ((CheckBox) sender).DataContext; + + Cost.AffectedFlats.Remove(flat); + } + + private void OnCostEntryDeleteClick(object sender, RoutedEventArgs e) + { + var entry = (CostEntryProperty) ((HighlightButton) sender).DataContext; + + Cost.Entries.Remove(entry); + } + + private void OnAddCostEntryClick(object sender, RoutedEventArgs e) + { + Cost.Entries.Add(); + } } } \ No newline at end of file diff --git a/ucalc/Data/Billing.cs b/ucalc/Data/Billing.cs index 961b3a5..bd9a0c9 100644 --- a/ucalc/Data/Billing.cs +++ b/ucalc/Data/Billing.cs @@ -296,7 +296,7 @@ namespace UCalc.Data public class CostEntryDetails { - public decimal TotalPrice { get; set; } + public decimal TotalAmount { get; set; } public decimal UnitCount { get; set; } public List DiscountsInUnits { get; private set; } @@ -307,7 +307,7 @@ namespace UCalc.Data private bool Equals(CostEntryDetails other) { - return TotalPrice == other.TotalPrice && UnitCount == other.UnitCount && + return TotalAmount == other.TotalAmount && UnitCount == other.UnitCount && DiscountsInUnits.SequenceEqual(other.DiscountsInUnits); } @@ -322,7 +322,7 @@ namespace UCalc.Data { return new CostEntryDetails { - TotalPrice = TotalPrice, + TotalAmount = TotalAmount, UnitCount = UnitCount, DiscountsInUnits = new List(DiscountsInUnits) }; @@ -333,7 +333,7 @@ namespace UCalc.Data { public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } - public decimal Price { get; set; } + public decimal Amount { get; set; } public CostEntryDetails Details { get; set; } public CostEntry() @@ -343,7 +343,7 @@ namespace UCalc.Data private bool Equals(CostEntry other) { - return StartDate.Equals(other.StartDate) && EndDate.Equals(other.EndDate) && Price == other.Price && + return StartDate.Equals(other.StartDate) && EndDate.Equals(other.EndDate) && Amount == other.Amount && Details.Equals(other.Details); } @@ -360,7 +360,7 @@ namespace UCalc.Data { StartDate = StartDate, EndDate = EndDate, - Price = Price, + Amount = Amount, Details = Details?.Clone() }; } @@ -373,6 +373,24 @@ namespace UCalc.Data Size } + public static class CostDivisions + { + public static string AsString(this CostDivision division) + { + switch (division) + { + case CostDivision.Person: + return "Pro Person"; + case CostDivision.Flat: + return "Pro Wohnung"; + case CostDivision.Size: + return "Pro m²"; + default: + throw new InvalidOperationException(); + } + } + } + public class Cost { public string Name { get; set; } diff --git a/ucalc/Models/CostEntriesProperty.cs b/ucalc/Models/CostEntriesProperty.cs new file mode 100644 index 0000000..35f7c7b --- /dev/null +++ b/ucalc/Models/CostEntriesProperty.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using UCalc.Data; + +namespace UCalc.Models +{ + public class CostEntriesProperty : MultiProperty + { + public CostEntriesProperty(Model model, Property parent, IEnumerable data) : base(model, parent, + "Zeiträume: Geben Sie einen oder mehr Zeiträume an.") + { + foreach (var entry in data) + { + Add(new CostEntryProperty(model, this, entry)); + } + } + + public void Add() + { + var entry = new CostEntryProperty(Model, this, new CostEntry()); + entry.StartDate.Value = null; + entry.EndDate.Value = null; + base.Add(entry); + } + + public new void Remove(CostEntryProperty entry) + { + using var validator = Model.BeginValidation(); + + base.Remove(entry); + + validator.ValidateRange(Properties.Select(otherEntry => otherEntry.EndDate)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/CostEntryProperty.cs b/ucalc/Models/CostEntryProperty.cs new file mode 100644 index 0000000..a224b00 --- /dev/null +++ b/ucalc/Models/CostEntryProperty.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using UCalc.Controls; +using UCalc.Data; + +namespace UCalc.Models +{ + public class DateProperty : ValueProperty + { + public DateProperty(Model model, Property parent, string name, DateTime value) : base(model, parent, name, + value) + { + } + + protected override string ValidateValue() + { + var error = Value == null ? $"{Name}: Geben Sie einen Wert ein." : ""; + + using var validator = Model.BeginValidation(); + validator.Validate(((CostEntryProperty) Parent).EndDate); + + return error; + } + } + + public class EndDateProperty : ValueProperty + { + public EndDateProperty(Model model, Property parent, string name, DateTime value) : base(model, parent, name, + value) + { + } + + protected override string ValidateValue() + { + try + { + if (Value == null) + { + return $"{Name}: Geben Sie einen Wert ein."; + } + + var startDate = ((CostEntryProperty) Parent).StartDate.Value; + if (startDate != null) + { + if (startDate > Value) + { + return $"{Name}: Das Enddatum liegt vor dem Startdatum."; + } + + foreach (var entry in ((CostProperty) Parent.Parent.Parent).Entries) + { + if (!ReferenceEquals(entry.EndDate, this) && entry.StartDate.Value != null && + entry.EndDate.Value != null && + startDate.Value.Intersects(Value.Value, entry.StartDate.Value.Value, + entry.EndDate.Value.Value)) + { + return $"{Name}: Dieser Zeitraum überschneidet sich mit einem anderen."; + } + } + } + + return ""; + } + finally + { + using var validator = Model.BeginValidation(); + validator.ValidateRange(((CostProperty) Parent.Parent.Parent).Entries.Select(entry => entry.EndDate)); + } + } + } + + public class CostEntryProperty : NestedProperty + { + public DateProperty StartDate { get; } + public EndDateProperty EndDate { get; } + public PositiveDecimalProperty Amount { get; } + + // TODO: Details + + public CostEntryProperty(Model model, Property parent, CostEntry data) : base(model, parent) + { + StartDate = Add(new DateProperty(model, this, "Startdatum", data.StartDate)); + EndDate = Add(new EndDateProperty(model, this, "Enddatum", data.EndDate)); + Amount = Add(new PositiveDecimalProperty(model, this, "Betrag", data.Amount)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/CostProperty.cs b/ucalc/Models/CostProperty.cs index ab409f7..fa2a0cf 100644 --- a/ucalc/Models/CostProperty.cs +++ b/ucalc/Models/CostProperty.cs @@ -1,18 +1,97 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using UCalc.Data; namespace UCalc.Models { + public class AffectsAllProperty : AlwaysValidProperty + { + public AffectsAllProperty(Model model, Property parent, string name, bool value) : base(model, parent, name, + value) + { + } + + protected override string ValidateValue() + { + using var validator = Model.BeginValidation(); + validator.Validate(((CostProperty) Parent).AffectedFlats); + return ""; + } + } + + public class AffectedFlatsProperty : Property + { + private const string NoFlatsError = "Betroffene Wohnungen: Es wurde keine Wohnung zugewiesen."; + private readonly HashSet _flats; + private readonly List _errors; + + public AffectedFlatsProperty(Model model, Property parent, IEnumerable data, + IReadOnlyDictionary flatToProperty = null) : base(model, parent) + { + _flats = new HashSet(); + _errors = new List(); + + foreach (var flat in data) + { + _flats.Add(flatToProperty?[flat] ?? throw new InvalidOperationException()); + } + + Validate(); + } + + public override IReadOnlyList Errors => _errors; + + public void Add(FlatProperty flat) + { + if (!_flats.Add(flat)) + { + return; + } + + using var validator = Model.BeginValidation(); + validator.Validate(this); + Modified = true; + } + + public void Remove(FlatProperty flat) + { + if (!_flats.Remove(flat)) + { + return; + } + + using var validator = Model.BeginValidation(); + validator.Validate(this); + Modified = true; + } + + public bool Contains(FlatProperty flat) + { + return _flats.Contains(flat); + } + + public sealed override void Validate() + { + _errors.Clear(); + + if (_flats.Count == 0 && !((CostProperty) Parent).AffectsAll.Value) + { + _errors.Add(NoFlatsError); + } + + using var validator = Model.BeginValidation(); + validator.Notify(this, "Errors"); + } + } + public class CostProperty : NestedProperty { public NotEmptyStringProperty Name { get; } public AlwaysValidProperty Division { get; } - public AlwaysValidProperty AffectsAll { get; } - + public AffectsAllProperty AffectsAll { get; } public AlwaysValidProperty IncludeUnrented { get; } - - // TODO: public HashSet AffectedFlats { get; } - // TODO: public List Entries { get; } + public AffectedFlatsProperty AffectedFlats { get; } + public CostEntriesProperty Entries { get; } public AlwaysValidProperty DisplayInBill { get; } public CostProperty(Model model, Property parent, Cost cost, @@ -20,9 +99,11 @@ namespace UCalc.Models { Name = Add(new NotEmptyStringProperty(model, this, "Name", cost.Name)); Division = Add(new AlwaysValidProperty(model, this, "Aufteilung", (int) cost.Division)); - AffectsAll = Add(new AlwaysValidProperty(model, this, "Betrifft alle", cost.AffectsAll)); + AffectsAll = Add(new AffectsAllProperty(model, this, "Betrifft alle", cost.AffectsAll)); + AffectedFlats = Add(new AffectedFlatsProperty(model, this, cost.AffectedFlats, flatToProperty)); IncludeUnrented = Add(new AlwaysValidProperty(model, this, "Unvermietete einbeziehen", cost.IncludeUnrented)); + Entries = Add(new CostEntriesProperty(model, this, cost.Entries)); DisplayInBill = Add(new AlwaysValidProperty(model, this, "In Rechnung anzeigen", cost.DisplayInBill)); } } diff --git a/ucalc/Models/TenantProperty.cs b/ucalc/Models/TenantProperty.cs index 1624e6a..224c71a 100644 --- a/ucalc/Models/TenantProperty.cs +++ b/ucalc/Models/TenantProperty.cs @@ -103,56 +103,61 @@ namespace UCalc.Models { _errors.Clear(); - if (_flatProperties.Count == 0) + try { - _errors.Add(NoFlatsError); - return; - } - - var entryDate = ((TenantProperty) Parent).EntryDate.Value; - var departureDate = ((TenantProperty) Parent).DepartureDate.Value; - - foreach (var flatProperty in _flatProperties) - { - foreach (var tenant in Model.Root.Tenants) + if (_flatProperties.Count == 0) { - if (ReferenceEquals(this, tenant.RentedFlats)) - { - continue; - } + _errors.Add(NoFlatsError); + return; + } - if (tenant.RentedFlats.IsRented(flatProperty, entryDate, departureDate)) - { - var error = $"Gemietete Wohnungen: Die Wohnung \"{flatProperty.Name.Value}\" ist bereits "; + var entryDate = ((TenantProperty) Parent).EntryDate.Value; + var departureDate = ((TenantProperty) Parent).DepartureDate.Value; - if (entryDate != null) + foreach (var flatProperty in _flatProperties) + { + foreach (var tenant in Model.Root.Tenants) + { + if (ReferenceEquals(this, tenant.RentedFlats)) { - if (departureDate != null) - { - _errors.Add( - $"von {entryDate.Value.ToString(Constants.DateFormat)} - {departureDate.Value.ToString(Constants.DateFormat)} "); - } - else - { - _errors.Add($"seit dem {entryDate.Value.ToString(Constants.DateFormat)} "); - } - } - else if (departureDate != null) - { - _errors.Add($"bis zum {departureDate.Value.ToString(Constants.DateFormat)} "); + continue; } - error += "an einen anderen Mieter vermietet."; - _errors.Add(error); - break; + if (tenant.RentedFlats.IsRented(flatProperty, entryDate, departureDate)) + { + var error = $"Gemietete Wohnungen: Die Wohnung \"{flatProperty.Name.Value}\" ist bereits "; + + if (entryDate != null) + { + if (departureDate != null) + { + _errors.Add( + $"von {entryDate.Value.ToString(Constants.DateFormat)} - {departureDate.Value.ToString(Constants.DateFormat)} "); + } + else + { + _errors.Add($"seit dem {entryDate.Value.ToString(Constants.DateFormat)} "); + } + } + else if (departureDate != null) + { + _errors.Add($"bis zum {departureDate.Value.ToString(Constants.DateFormat)} "); + } + + error += "an einen anderen Mieter vermietet."; + _errors.Add(error); + break; + } } } } + finally + { + using var validator = Model.BeginValidation(); + validator.Notify(this, "Errors"); - using var validator = Model.BeginValidation(); - validator.Notify(this, "Errors"); - - validator.ValidateRange(Model.Root.Tenants); + validator.ValidateRange(Model.Root.Tenants); + } } private bool IsRented(FlatProperty flat, DateTime? start, DateTime? end)