From 1bd610fe72167e4bdfba8594c9cd6b5695ebf785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Wed, 15 Jul 2026 21:51:31 +0200 Subject: [PATCH 01/18] Add base resource planner class --- .../ResourcePlanner/ResourcePlanner.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs new file mode 100644 index 00000000..ddf9fac8 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs @@ -0,0 +1,53 @@ +using Turnierplan.Core.Entity; +using Turnierplan.Core.RoleAssignment; + +namespace Turnierplan.Core.ResourcePlanner; + +public sealed class ResourcePlanner : Entity, IEntityWithRoleAssignments, IEntityWithOrganization +{ + internal readonly List> _roleAssignments = []; + + public ResourcePlanner(Organization.Organization organization, string name) + { + organization._resourcePlanners.Add(this); + + Id = 0; + PublicId = new PublicId.PublicId(); + Organization = organization; + CreatedAt = DateTime.UtcNow; + Name = name; + } + + internal ResourcePlanner(long id, PublicId.PublicId publicId, DateTime createdAt, string name) + { + Id = id; + PublicId = publicId; + CreatedAt = createdAt; + Name = name; + } + + public override long Id { get; protected set; } + + public PublicId.PublicId PublicId { get; } + + public Organization.Organization Organization { get; internal set; } = null!; + + public IReadOnlyList> RoleAssignments => _roleAssignments.AsReadOnly(); + + public DateTime CreatedAt { get; } + + public string Name { get; } + + public RoleAssignment AddRoleAssignment(Role role, Principal principal) + { + var roleAssignment = new RoleAssignment(this, role, principal); + _roleAssignments.Add(roleAssignment); + + return roleAssignment; + } + + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } +} From 9d963dd5512b2be12a5b2c3f53f58dda231aa2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Wed, 15 Jul 2026 21:57:43 +0200 Subject: [PATCH 02/18] Add entity type config & integrate into organization --- .../Organization/Organization.cs | 3 ++ .../OrganizationEntityTypeConfiguration.cs | 7 ++++ .../ResourcePlannerEntityTypeConfiguration.cs | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs diff --git a/src/Turnierplan.Core/Organization/Organization.cs b/src/Turnierplan.Core/Organization/Organization.cs index bf731f6a..d9c2f707 100644 --- a/src/Turnierplan.Core/Organization/Organization.cs +++ b/src/Turnierplan.Core/Organization/Organization.cs @@ -9,6 +9,7 @@ public sealed class Organization : Entity, IEntityWithRoleAssignments _apiKeys = []; internal readonly List _folders = []; internal readonly List _images = []; + internal readonly List _resourcePlanners = []; internal readonly List _tournamentPlanners = []; internal readonly List _tournaments = []; internal readonly List _venues = []; @@ -45,6 +46,8 @@ internal Organization(long id, PublicId.PublicId publicId, DateTime createdAt, s public IReadOnlyList Images => _images.AsReadOnly(); + public IReadOnlyList ResourcePlanners => _resourcePlanners.AsReadOnly(); + public IReadOnlyList TournamentPlanners => _tournamentPlanners.AsReadOnly(); public IReadOnlyList Tournaments => _tournaments.AsReadOnly(); diff --git a/src/Turnierplan.Dal/EntityConfigurations/OrganizationEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/OrganizationEntityTypeConfiguration.cs index cf3cfd76..2f82af0a 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/OrganizationEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/OrganizationEntityTypeConfiguration.cs @@ -52,6 +52,12 @@ public void Configure(EntityTypeBuilder builder) .OnDelete(DeleteBehavior.Restrict) .IsRequired(); + builder.HasMany(x => x.ResourcePlanners) + .WithOne(x => x.Organization) + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + builder.HasMany(x => x.TournamentPlanners) .WithOne(x => x.Organization) .HasForeignKey("OrganizationId") @@ -74,6 +80,7 @@ public void Configure(EntityTypeBuilder builder) builder.Metadata.FindNavigation(nameof(Organization.ApiKeys))!.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Metadata.FindNavigation(nameof(Organization.Folders))!.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Metadata.FindNavigation(nameof(Organization.Images))!.SetPropertyAccessMode(PropertyAccessMode.Field); + builder.Metadata.FindNavigation(nameof(Organization.ResourcePlanners))!.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Metadata.FindNavigation(nameof(Organization.TournamentPlanners))!.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Metadata.FindNavigation(nameof(Organization.Tournaments))!.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Metadata.FindNavigation(nameof(Organization.Venues))!.SetPropertyAccessMode(PropertyAccessMode.Field); diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs new file mode 100644 index 00000000..e9b1e7a3 --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Turnierplan.Core.ResourcePlanner; +using Turnierplan.Dal.Converters; + +namespace Turnierplan.Dal.EntityConfigurations; + +public sealed class ResourcePlannerEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ResourcePlanners", TurnierplanContext.Schema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.PublicId) + .HasConversion(); + + builder.HasIndex(x => x.PublicId) + .IsUnique(); + + builder.HasMany(x => x.RoleAssignments) + .WithOne(x => x.Scope) + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + builder.Property(x => x.CreatedAt) + .IsRequired(); + + builder.Property(x => x.Name) + .IsRequired(); + + builder.Metadata.FindNavigation(nameof(ResourcePlanner.RoleAssignments))!.SetPropertyAccessMode(PropertyAccessMode.Field); + } +} From f009609095db58dde898eca7b0248a26ec783d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Wed, 15 Jul 2026 22:38:50 +0200 Subject: [PATCH 03/18] Add first rough draft of model --- src/Turnierplan.Core/ResourcePlanner/Model.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/Turnierplan.Core/ResourcePlanner/Model.cs diff --git a/src/Turnierplan.Core/ResourcePlanner/Model.cs b/src/Turnierplan.Core/ResourcePlanner/Model.cs new file mode 100644 index 00000000..1f9f5dc1 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/Model.cs @@ -0,0 +1,56 @@ +namespace Turnierplan.Core.ResourcePlanner; + +public sealed class Column +{ + // - Type => "Day" or "General" + // - OrderIndex + // - Name (optional) + // - Description (optional) + + // if type == "Day": + // - Date (yyyy/mm/dd) + + List Groups = []; +} + +public sealed class Group +{ + // - Type => "Shift" or "General" + // - Name (optional / mandatory if type == "General") + // - Description (optional) + + // if type == "Shift": + // - Start Timestamp + // - End Timestamp + + List ResourceAssignments = []; + List ExternalViewAssignment = []; +} + +public sealed class Resource +{ + // - Type => "Personnel" or "Commodity" + // - Name + // - Description/Notes (optional) +} + +public sealed class ResourceAssignment +{ + // - State => "Proposed" / "Requested" / "Confirmed" / "..." [custom?] + + Group Group; + Resource Resource; +} + +public sealed class ExternalView +{ + // - Public ID (for externally visible URL) + // - IsEnabled (display 404 externally when false) + // - DisplayAllGroups (if false, assigned groups are configured via Group.ExternalViewAssignments) +} + +public sealed class ExternalViewAssignment +{ + Group Group; + ExternalView ExternalView; +} From 8d839d10e584bc74e2f76f9bfb6e39ebfa978961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Thu, 16 Jul 2026 22:25:48 +0200 Subject: [PATCH 04/18] First refinement of model --- src/Turnierplan.Core/ResourcePlanner/Model.cs | 103 ++++++++++++------ 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/src/Turnierplan.Core/ResourcePlanner/Model.cs b/src/Turnierplan.Core/ResourcePlanner/Model.cs index 1f9f5dc1..7b00a823 100644 --- a/src/Turnierplan.Core/ResourcePlanner/Model.cs +++ b/src/Turnierplan.Core/ResourcePlanner/Model.cs @@ -1,56 +1,95 @@ +using Turnierplan.Core.Entity; + namespace Turnierplan.Core.ResourcePlanner; -public sealed class Column +public sealed class ResourceGroup : Entity { - // - Type => "Day" or "General" - // - OrderIndex - // - Name (optional) - // - Description (optional) + // todo: List ResourceAssignments = []; + // todo: List ExternalViewAssignment = []; + + public override long Id { get; protected set; } + + public ResourcePlanner ResourcePlanner { get; internal set; } = null!; + + public ResourceGroupType Type { get; } + + public DateTime? Start { get; } // only for type=shift + + public DateTime? End { get; } // only for type=shift - // if type == "Day": - // - Date (yyyy/mm/dd) + public string Name { get; } // mandatory for type=general - List Groups = []; + public string Description { get; } // can be empty? } -public sealed class Group +public enum ResourceGroupType { - // - Type => "Shift" or "General" - // - Name (optional / mandatory if type == "General") - // - Description (optional) + // Note: Don't change enum values (DB serialization) + + Shift = 1, // todo: name? + General = 2, // todo: name? + // ...todo: others? +} + +public sealed class Resource : Entity +{ + public override long Id { get; protected set; } + + public ResourcePlanner ResourcePlanner { get; internal set; } = null!; + + public ResourceType Type { get; } - // if type == "Shift": - // - Start Timestamp - // - End Timestamp + public string Name { get; } - List ResourceAssignments = []; - List ExternalViewAssignment = []; + public string Notes { get; } // can be empty? } -public sealed class Resource +public enum ResourceType { - // - Type => "Personnel" or "Commodity" - // - Name - // - Description/Notes (optional) + // Note: Don't change enum values (DB serialization) + + Personnel = 1, // todo: name? + Commodity = 2, // todo: name? + // ...todo: others? } -public sealed class ResourceAssignment +public sealed class ResourceAssignment // analogous to group participant { - // - State => "Proposed" / "Requested" / "Confirmed" / "..." [custom?] + public ResourceGroup ResourceGroup { get; internal set; } = null!; + + public Resource Resource { get; internal set; } = null!; - Group Group; - Resource Resource; + public ResourceAssignmentState State { get; } } -public sealed class ExternalView +public enum ResourceAssignmentState { - // - Public ID (for externally visible URL) - // - IsEnabled (display 404 externally when false) - // - DisplayAllGroups (if false, assigned groups are configured via Group.ExternalViewAssignments) + Proposed = 1, + Requested = 2, + Confirmed = 3, + // ...todo: others? } -public sealed class ExternalViewAssignment +public sealed class ResourcePlannerView : Entity { - Group Group; - ExternalView ExternalView; + public override long Id { get; protected set; } + + public PublicId.PublicId PublicId { get; } // (for externally visible URL) + + public ResourcePlanner ResourcePlanner { get; internal set; } = null!; + + public bool IsActive { get; set; } // (display 404 externally when false) + + public bool DisplayAllGroups { get; set; } // (if false, assigned groups are configured via Group.ExternalViewAssignments) + + // todo: Valid until? + // todo: Primary/Secondary Images? +} + +// maybe not necessary if no further properties are needed... then EF can implicitly create the join table +public sealed class ExternalViewAssignment // analogous to group participant +{ + public ResourcePlannerView View { get; internal set; } = null!; + + public ResourceGroup ResourceGroup { get; internal set; } = null!; } From 5c6d9b44c3f37489037d2f72b1ed2694d64aeddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Fri, 17 Jul 2026 19:57:33 +0200 Subject: [PATCH 05/18] Split files and build coherent database model --- src/Turnierplan.Core/ResourcePlanner/Model.cs | 95 ---------- .../ResourcePlanner/Resource.cs | 55 ++++++ .../ResourcePlanner/ResourceAssignment.cs | 22 +++ .../ResourceAssignmentState.cs | 9 + .../ResourcePlanner/ResourceGroup.cs | 164 ++++++++++++++++++ .../ResourcePlanner/ResourceGroupType.cs | 9 + .../ResourcePlanner/ResourcePlanner.cs | 32 ++++ .../ResourcePlanner/ResourcePlannerView.cs | 65 +++++++ .../ResourcePlanner/ResourceType.cs | 10 ++ 9 files changed, 366 insertions(+), 95 deletions(-) delete mode 100644 src/Turnierplan.Core/ResourcePlanner/Model.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/Resource.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourcePlannerView.cs create mode 100644 src/Turnierplan.Core/ResourcePlanner/ResourceType.cs diff --git a/src/Turnierplan.Core/ResourcePlanner/Model.cs b/src/Turnierplan.Core/ResourcePlanner/Model.cs deleted file mode 100644 index 7b00a823..00000000 --- a/src/Turnierplan.Core/ResourcePlanner/Model.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Turnierplan.Core.Entity; - -namespace Turnierplan.Core.ResourcePlanner; - -public sealed class ResourceGroup : Entity -{ - // todo: List ResourceAssignments = []; - // todo: List ExternalViewAssignment = []; - - public override long Id { get; protected set; } - - public ResourcePlanner ResourcePlanner { get; internal set; } = null!; - - public ResourceGroupType Type { get; } - - public DateTime? Start { get; } // only for type=shift - - public DateTime? End { get; } // only for type=shift - - public string Name { get; } // mandatory for type=general - - public string Description { get; } // can be empty? -} - -public enum ResourceGroupType -{ - // Note: Don't change enum values (DB serialization) - - Shift = 1, // todo: name? - General = 2, // todo: name? - // ...todo: others? -} - -public sealed class Resource : Entity -{ - public override long Id { get; protected set; } - - public ResourcePlanner ResourcePlanner { get; internal set; } = null!; - - public ResourceType Type { get; } - - public string Name { get; } - - public string Notes { get; } // can be empty? -} - -public enum ResourceType -{ - // Note: Don't change enum values (DB serialization) - - Personnel = 1, // todo: name? - Commodity = 2, // todo: name? - // ...todo: others? -} - -public sealed class ResourceAssignment // analogous to group participant -{ - public ResourceGroup ResourceGroup { get; internal set; } = null!; - - public Resource Resource { get; internal set; } = null!; - - public ResourceAssignmentState State { get; } -} - -public enum ResourceAssignmentState -{ - Proposed = 1, - Requested = 2, - Confirmed = 3, - // ...todo: others? -} - -public sealed class ResourcePlannerView : Entity -{ - public override long Id { get; protected set; } - - public PublicId.PublicId PublicId { get; } // (for externally visible URL) - - public ResourcePlanner ResourcePlanner { get; internal set; } = null!; - - public bool IsActive { get; set; } // (display 404 externally when false) - - public bool DisplayAllGroups { get; set; } // (if false, assigned groups are configured via Group.ExternalViewAssignments) - - // todo: Valid until? - // todo: Primary/Secondary Images? -} - -// maybe not necessary if no further properties are needed... then EF can implicitly create the join table -public sealed class ExternalViewAssignment // analogous to group participant -{ - public ResourcePlannerView View { get; internal set; } = null!; - - public ResourceGroup ResourceGroup { get; internal set; } = null!; -} diff --git a/src/Turnierplan.Core/ResourcePlanner/Resource.cs b/src/Turnierplan.Core/ResourcePlanner/Resource.cs new file mode 100644 index 00000000..a2d32f6d --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/Resource.cs @@ -0,0 +1,55 @@ +using Turnierplan.Core.Entity; +using Turnierplan.Core.Exceptions; + +namespace Turnierplan.Core.ResourcePlanner; + +public sealed class Resource : Entity +{ + private string _name; + private string? _notes; + + internal Resource(long id, ResourceType type, string name, string? notes) + { + Id = id; + Type = type; + _name = name; + _notes = notes; + } + + public Resource(ResourcePlanner resourcePlanner, ResourceType type, string name, string? notes) + { + Id = 0; + ResourcePlanner = resourcePlanner; + Type = type; + _name = name; + _notes = notes; + } + + public override long Id { get; protected set; } + + public ResourcePlanner ResourcePlanner { get; internal set; } = null!; + + public ResourceType Type { get; set; } + + public string Name + { + get => _name; + set => _name = value.Trim(); + } + + public string? Notes + { + get => _notes; + set + { + var trimmed = value?.Trim(); + + if (trimmed is not null && trimmed.Length == 0) + { + throw new TurnierplanException($"{nameof(Notes)} must be null or a non-empty string"); + } + + _notes = trimmed; + } + } +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs new file mode 100644 index 00000000..bc5f8e66 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs @@ -0,0 +1,22 @@ +namespace Turnierplan.Core.ResourcePlanner; + +public sealed class ResourceAssignment +{ + internal ResourceAssignment(ResourceAssignmentState state) + { + State = state; + } + + internal ResourceAssignment(ResourceGroup resourceGroup, Resource resource) + { + ResourceGroup = resourceGroup; + Resource = resource; + State = ResourceAssignmentState.Confirmed; // Todo: What's the default state for a newly assigned resource? + } + + public ResourceGroup ResourceGroup { get; internal set; } = null!; + + public Resource Resource { get; internal set; } = null!; + + public ResourceAssignmentState State { get; set; } +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs new file mode 100644 index 00000000..c05bec9f --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs @@ -0,0 +1,9 @@ +namespace Turnierplan.Core.ResourcePlanner; + +public enum ResourceAssignmentState +{ + Proposed = 1, + Requested = 2, + Confirmed = 3, + // ...Todo: Do we need these states, additional ones, other ones? +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs new file mode 100644 index 00000000..6c89f945 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs @@ -0,0 +1,164 @@ +using Turnierplan.Core.Entity; +using Turnierplan.Core.Exceptions; + +namespace Turnierplan.Core.ResourcePlanner; + +public sealed class ResourceGroup : Entity +{ + internal readonly List _resourceAssignments = []; + + private string? _name; + private string? _description; + + internal ResourceGroup(long id, string? name, string? description, ResourceGroupType type, DateTime? start, DateTime? end) + { + Id = id; + Type = type; + Start = start; + End = end; + + _name = name; + _description = description; + } + + internal ResourceGroup(ResourcePlanner resourcePlanner, string? name, string? description, ResourceGroupType type, DateTime? start, DateTime? end) + { + var isWorkshift = type is ResourceGroupType.Workshift; + var isGeneral = type is ResourceGroupType.General; + + if (!isWorkshift && !isGeneral) + { + throw new TurnierplanException($"Invalid resource type: '{type}'"); + } + + if (isWorkshift) + { + if (!start.HasValue || end.HasValue) + { + throw new TurnierplanException($"Start and end time must be set if the type is '{ResourceGroupType.Workshift}'"); + } + } + + if (isGeneral) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new TurnierplanException($"Name must be a non-empty string if the type is '{ResourceGroupType.General}'"); + } + } + + Id = 0; + ResourcePlanner = resourcePlanner; + Type = type; + Start = isWorkshift ? start : null; + End = isWorkshift ? end : null; + + _name = name; + _description = description; + } + + public override long Id { get; protected set; } + + public ResourcePlanner ResourcePlanner { get; internal set; } = null!; + + public string? Name + { + get => _name; + set + { + var trimmed = value?.Trim(); + + if (Type is ResourceGroupType.General) + { + if (string.IsNullOrEmpty(trimmed)) + { + throw new TurnierplanException($"If the {nameof(Type)} is {ResourceGroupType.General}, the {nameof(Name)} must be a non-empty string."); + } + } + else if (trimmed is not null && trimmed.Length == 0) + { + throw new TurnierplanException($"{nameof(Name)} must be null or a non-empty string"); + } + + _name = trimmed; + } + } + + public string? Description + { + get => _description; + set + { + var trimmed = value?.Trim(); + + if (trimmed is not null && trimmed.Length == 0) + { + throw new TurnierplanException($"{nameof(Description)} must be null or a non-empty string"); + } + + _description = trimmed; + } + } + + public ResourceGroupType Type { get; } + + public DateTime? Start + { + get; + set + { + if (Type is ResourceGroupType.Workshift && value is null) + { + throw new TurnierplanException($"Start time may not be set to null if type is {ResourceGroupType.Workshift}."); + } + + if (Type is not ResourceGroupType.Workshift && value is not null) + { + throw new TurnierplanException($"Start time may not be set to a non-null value if type is not {ResourceGroupType.Workshift}."); + } + + field = value; + } + } + + public DateTime? End + { + get; + set + { + if (Type is ResourceGroupType.Workshift && value is null) + { + throw new TurnierplanException($"End time may not be set to null if type is {ResourceGroupType.Workshift}."); + } + + if (Type is not ResourceGroupType.Workshift && value is not null) + { + throw new TurnierplanException($"End time may not be set to a non-null value if type is not {ResourceGroupType.Workshift}."); + } + + field = value; + } + } + + public IReadOnlyList ResourceAssignments => _resourceAssignments.AsReadOnly(); + + public void AssignResource(Resource resource) + { + if (resource.ResourcePlanner != ResourcePlanner) + { + throw new TurnierplanException("Cannot assign resource from another resource planner."); + } + + if (_resourceAssignments.Any(x => x.Resource == resource)) + { + throw new TurnierplanException("The specified resource is already assigned to this resource group."); + } + + _resourceAssignments.Add(new ResourceAssignment(this, resource)); + } + + public void UnassignResource(ResourceAssignment assignment) + { + _resourceAssignments.Remove(assignment); + } +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs new file mode 100644 index 00000000..0b43fbd5 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs @@ -0,0 +1,9 @@ +namespace Turnierplan.Core.ResourcePlanner; + +public enum ResourceGroupType +{ + // Note: Don't change enum values (DB serialization) + + Workshift = 1, // Todo: Name? + General = 2 // Todo: Name? +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs index ddf9fac8..7001a54e 100644 --- a/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs +++ b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs @@ -6,6 +6,8 @@ namespace Turnierplan.Core.ResourcePlanner; public sealed class ResourcePlanner : Entity, IEntityWithRoleAssignments, IEntityWithOrganization { internal readonly List> _roleAssignments = []; + internal readonly List _resourceGroups = []; + internal readonly List _resourcePlannerViews = []; public ResourcePlanner(Organization.Organization organization, string name) { @@ -34,6 +36,10 @@ internal ResourcePlanner(long id, PublicId.PublicId publicId, DateTime createdAt public IReadOnlyList> RoleAssignments => _roleAssignments.AsReadOnly(); + public IReadOnlyList ResourceGroups => _resourceGroups.AsReadOnly(); + + public IReadOnlyList ResourcePlannerViews => _resourcePlannerViews.AsReadOnly(); + public DateTime CreatedAt { get; } public string Name { get; } @@ -50,4 +56,30 @@ public void RemoveRoleAssignment(RoleAssignment roleAssignment) { _roleAssignments.Remove(roleAssignment); } + + public ResourceGroup AddResourceGroup(string? name, string? description, ResourceGroupType type, DateTime? start, DateTime? end) + { + var resourceGroup = new ResourceGroup(this, name, description, type, start, end); + _resourceGroups.Add(resourceGroup); + + return resourceGroup; + } + + public void RemoveResourceGroup(ResourceGroup resourceGroup) + { + _resourceGroups.Remove(resourceGroup); + } + + public ResourcePlannerView AddResourcePlannerView(bool isActive, bool displayAllGroups) + { + var view = new ResourcePlannerView(this, isActive, displayAllGroups); + _resourcePlannerViews.Add(view); + + return view; + } + + public void RemoveResourcePlannerView(ResourcePlannerView resourcePlannerView) + { + _resourcePlannerViews.Remove(resourcePlannerView); + } } diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourcePlannerView.cs b/src/Turnierplan.Core/ResourcePlanner/ResourcePlannerView.cs new file mode 100644 index 00000000..f262e50a --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourcePlannerView.cs @@ -0,0 +1,65 @@ +using Turnierplan.Core.Entity; +using Turnierplan.Core.Exceptions; + +namespace Turnierplan.Core.ResourcePlanner; + +// Todo: Add 'valid until' date? +// Todo: Add option to show primary/secondary image? +public sealed class ResourcePlannerView : Entity +{ + internal readonly List _resourceGroups = []; + + internal ResourcePlannerView(long id, PublicId.PublicId publicId, bool isActive, bool displayAllGroups) + { + Id = id; + PublicId = publicId; + IsActive = isActive; + DisplayAllGroups = displayAllGroups; + } + + internal ResourcePlannerView(ResourcePlanner resourcePlanner, bool isActive, bool displayAllGroups) + { + Id = 0; + PublicId = new PublicId.PublicId(); + ResourcePlanner = resourcePlanner; + IsActive = isActive; + DisplayAllGroups = displayAllGroups; + } + + public override long Id { get; protected set; } + + public PublicId.PublicId PublicId { get; } + + public ResourcePlanner ResourcePlanner { get; internal set; } = null!; + + public bool IsActive { get; set; } + + public bool DisplayAllGroups { get; set; } + + public IReadOnlyList ResourceGroups => _resourceGroups.AsReadOnly(); + + public void AddResourceGroup(ResourceGroup resourceGroup) + { + if (resourceGroup.ResourcePlanner != ResourcePlanner) + { + throw new TurnierplanException("Cannot assign resource group from another resource planner."); + } + + if (DisplayAllGroups) + { + throw new TurnierplanException("Cannot assign resource group because this resource planner view is currently configured to display all resource groups."); + } + + if (_resourceGroups.Contains(resourceGroup)) + { + throw new TurnierplanException("The specified resource group is already included in this resource planner view."); + } + + _resourceGroups.Add(resourceGroup); + } + + public void RemoveResourceGroup(ResourceGroup resourceGroup) + { + _resourceGroups.Remove(resourceGroup); + } +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs new file mode 100644 index 00000000..83b2ada5 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs @@ -0,0 +1,10 @@ +namespace Turnierplan.Core.ResourcePlanner; + +public enum ResourceType +{ + // Note: Don't change enum values (DB serialization) + + Personnel = 1, + Commodity = 2, + // ...Todo: Do we need other resource types? +} From 119250f51ed311fd64aeec267074eaeb128afa33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Fri, 17 Jul 2026 20:11:22 +0200 Subject: [PATCH 06/18] Add first version of entity configs --- ...sourceAssignmentEntityTypeConfiguration.cs | 18 +++++++++ .../ResourceEntityTypeConfiguration.cs | 32 ++++++++++++++++ .../ResourceGroupEntityTypeConfiguration.cs | 37 +++++++++++++++++++ .../ResourcePlannerEntityTypeConfiguration.cs | 18 ++++++++- ...ourcePlannerViewEntityTypeConfiguration.cs | 23 ++++++++++++ src/Turnierplan.Dal/TurnierplanContext.cs | 7 ++++ 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs create mode 100644 src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs create mode 100644 src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs create mode 100644 src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs new file mode 100644 index 00000000..e4f4c5dc --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.Dal.EntityConfigurations; + +internal sealed class ResourceAssignmentEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ResourceAssignments", TurnierplanContext.Schema); + + builder.HasKey("ResourcePlannerId", "ResourceGroupId", "ResourceId"); + + builder.Property(x => x.State) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs new file mode 100644 index 00000000..8bc9910b --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.Dal.EntityConfigurations; + +internal sealed class ResourceEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Resources", TurnierplanContext.Schema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.Type) + .IsRequired(); + + builder.Property(x => x.Name) + .IsRequired(); + + builder.Property(x => x.Notes); + + builder.HasMany() + .WithOne(x => x.Resource) + .HasForeignKey("ResourcePlannerId", "ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs new file mode 100644 index 00000000..eec21dc9 --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.Dal.EntityConfigurations; + +internal sealed class ResourceGroupEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ResourceGroups", TurnierplanContext.Schema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.Property(x => x.Name); + + builder.Property(x => x.Description); + + builder.Property(x => x.Type) + .IsRequired(); + + builder.Property(x => x.Start); + + builder.Property(x => x.End); + + builder.HasMany(x => x.ResourceAssignments) + .WithOne(x => x.ResourceGroup) + .HasForeignKey("ResourcePlannerId", "ResourceGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + builder.Metadata.FindNavigation(nameof(ResourceGroup.ResourceAssignments))!.SetPropertyAccessMode(PropertyAccessMode.Field); + } +} \ No newline at end of file diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs index e9b1e7a3..9cecc856 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs @@ -22,18 +22,32 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(x => x.PublicId) .IsUnique(); + builder.Property(x => x.CreatedAt) + .IsRequired(); + + builder.Property(x => x.Name) + .IsRequired(); + builder.HasMany(x => x.RoleAssignments) .WithOne(x => x.Scope) .HasForeignKey("ResourcePlannerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - builder.Property(x => x.CreatedAt) + builder.HasMany(x => x.ResourceGroups) + .WithOne(x => x.ResourcePlanner) + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - builder.Property(x => x.Name) + builder.HasMany(x => x.ResourcePlannerViews) + .WithOne(x => x.ResourcePlanner) + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); builder.Metadata.FindNavigation(nameof(ResourcePlanner.RoleAssignments))!.SetPropertyAccessMode(PropertyAccessMode.Field); + builder.Metadata.FindNavigation(nameof(ResourcePlanner.ResourceGroups))!.SetPropertyAccessMode(PropertyAccessMode.Field); + builder.Metadata.FindNavigation(nameof(ResourcePlanner.ResourcePlannerViews))!.SetPropertyAccessMode(PropertyAccessMode.Field); } } diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs new file mode 100644 index 00000000..f203c6f6 --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.Dal.EntityConfigurations; + +internal sealed class ResourcePlannerViewEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ResourcePlannerViews", TurnierplanContext.Schema); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .IsRequired(); + + builder.HasMany(x => x.ResourceGroups) + .WithMany(); + + builder.Metadata.FindNavigation(nameof(ResourcePlannerView.ResourceGroups))!.SetPropertyAccessMode(PropertyAccessMode.Field); + } +} diff --git a/src/Turnierplan.Dal/TurnierplanContext.cs b/src/Turnierplan.Dal/TurnierplanContext.cs index 749858e0..b2d277a0 100644 --- a/src/Turnierplan.Dal/TurnierplanContext.cs +++ b/src/Turnierplan.Dal/TurnierplanContext.cs @@ -6,6 +6,7 @@ using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; @@ -169,6 +170,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new MatchEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new OrganizationEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new RankingOverwriteEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ResourceAssignmentEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ResourceEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ResourceGroupEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ResourcePlannerEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ResourcePlannerViewEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new TeamEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new TeamLinkEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new TournamentEntityTypeConfiguration()); @@ -181,6 +187,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration("IAM_PlanningRealm")); // see to-do comment in TournamentPlannerEntityTypeConfiguration modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new RoleAssignmentEntityTypeConfiguration()); From adc9f684640dc2df5ed48939bd077ba84f9cd47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Fri, 17 Jul 2026 20:20:33 +0200 Subject: [PATCH 07/18] Fix some issues with entity type configs --- .../ResourceAssignmentEntityTypeConfiguration.cs | 4 ++-- .../ResourceEntityTypeConfiguration.cs | 4 ++-- .../ResourceGroupEntityTypeConfiguration.cs | 4 ++-- .../ResourcePlannerViewEntityTypeConfiguration.cs | 12 ++++++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs index e4f4c5dc..1110265a 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs @@ -10,9 +10,9 @@ public void Configure(EntityTypeBuilder builder) { builder.ToTable("ResourceAssignments", TurnierplanContext.Schema); - builder.HasKey("ResourcePlannerId", "ResourceGroupId", "ResourceId"); + builder.HasKey("ResourceGroupId", "ResourceId"); builder.Property(x => x.State) .IsRequired(); } -} \ No newline at end of file +} diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs index 8bc9910b..bf036ffe 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs @@ -25,8 +25,8 @@ public void Configure(EntityTypeBuilder builder) builder.HasMany() .WithOne(x => x.Resource) - .HasForeignKey("ResourcePlannerId", "ResourceId") + .HasForeignKey("ResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); } -} \ No newline at end of file +} diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs index eec21dc9..08a47662 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs @@ -28,10 +28,10 @@ public void Configure(EntityTypeBuilder builder) builder.HasMany(x => x.ResourceAssignments) .WithOne(x => x.ResourceGroup) - .HasForeignKey("ResourcePlannerId", "ResourceGroupId") + .HasForeignKey("ResourceGroupId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); builder.Metadata.FindNavigation(nameof(ResourceGroup.ResourceAssignments))!.SetPropertyAccessMode(PropertyAccessMode.Field); } -} \ No newline at end of file +} diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs index f203c6f6..b82ed9e7 100644 --- a/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Turnierplan.Core.ResourcePlanner; +using Turnierplan.Dal.Converters; namespace Turnierplan.Dal.EntityConfigurations; @@ -15,9 +16,16 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Id) .IsRequired(); + builder.Property(x => x.PublicId) + .HasConversion(); + + builder.Property(x => x.IsActive) + .IsRequired(); + + builder.Property(x => x.DisplayAllGroups) + .IsRequired(); + builder.HasMany(x => x.ResourceGroups) .WithMany(); - - builder.Metadata.FindNavigation(nameof(ResourcePlannerView.ResourceGroups))!.SetPropertyAccessMode(PropertyAccessMode.Field); } } From c2f4524cdee926a4f14aad15cafbc373dc56026c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Fri, 17 Jul 2026 20:20:48 +0200 Subject: [PATCH 08/18] Add initial migration --- ...0717182006_Add_ResourcePlanner.Designer.cs | 2167 +++++++++++++++++ .../20260717182006_Add_ResourcePlanner.cs | 273 +++ .../TurnierplanContextModelSnapshot.cs | 283 +++ 3 files changed, 2723 insertions(+) create mode 100644 src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs create mode 100644 src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs diff --git a/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs b/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs new file mode 100644 index 00000000..bbf5e99f --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs @@ -0,0 +1,2167 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Turnierplan.Dal; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + [DbContext(typeof(TurnierplanContext))] + [Migration("20260717182006_Add_ResourcePlanner")] + partial class Add_ResourcePlanner + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationTeamLabel", b => + { + b.Property("ApplicationTeamId") + .HasColumnType("bigint"); + + b.Property("LabelsId") + .HasColumnType("bigint"); + + b.HasKey("ApplicationTeamId", "LabelsId"); + + b.HasIndex("LabelsId"); + + b.ToTable("ApplicationTeamLabel", "turnierplan"); + }); + + modelBuilder.Entity("ResourceGroupResourcePlannerView", b => + { + b.Property("ResourceGroupsId") + .HasColumnType("bigint"); + + b.Property("ResourcePlannerViewId") + .HasColumnType("bigint"); + + b.HasKey("ResourceGroupsId", "ResourcePlannerViewId"); + + b.HasIndex("ResourcePlannerViewId"); + + b.ToTable("ResourceGroupResourcePlannerView", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PrincipalId") + .HasColumnType("uuid"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("SecretHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PrincipalId") + .IsUnique(); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("ApiKeys", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKeyRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKeyId") + .HasColumnType("bigint"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ApiKeyRequests", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Document.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Configuration") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GenerationCount") + .HasColumnType("integer"); + + b.Property("LastGeneration") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("TournamentId"); + + b.ToTable("Documents", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Folders", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileExtension") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("ResourceIdentifier") + .HasColumnType("uuid"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("ResourceIdentifier") + .IsUnique(); + + b.ToTable("Images", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Organization.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Organizations", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.Resource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("Resources", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceAssignment", b => + { + b.Property("ResourceGroupId") + .HasColumnType("bigint"); + + b.Property("ResourceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("ResourceGroupId", "ResourceId"); + + b.HasIndex("ResourceId"); + + b.ToTable("ResourceAssignments", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("ResourceGroups", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("ResourcePlanners", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlannerView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayAllGroups") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("ResourcePlannerViews", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId"); + + b.ToTable("IAM_ApiKey", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FolderId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FolderId"); + + b.ToTable("IAM_Folder", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("IAM_Image", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("IAM_Organization", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("IAM_ResourcePlanner", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TournamentId"); + + b.ToTable("IAM_Tournament", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("IAM_PlanningRealm", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("VenueId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("VenueId"); + + b.ToTable("IAM_Venue", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AlphabeticalId") + .HasColumnType("character(1)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("TournamentId", "Id"); + + b.ToTable("Groups", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.GroupParticipant", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.HasKey("TournamentId", "GroupId", "TeamId"); + + b.HasIndex("TournamentId", "TeamId"); + + b.ToTable("GroupParticipants", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Match", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Court") + .HasColumnType("smallint"); + + b.Property("FinalsRound") + .HasColumnType("integer"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("IsCurrentlyPlaying") + .HasColumnType("boolean"); + + b.Property("Kickoff") + .HasColumnType("timestamp with time zone"); + + b.Property("OutcomeType") + .HasColumnType("integer"); + + b.Property("PlayoffPosition") + .HasColumnType("integer"); + + b.Property("ScoreA") + .HasColumnType("integer"); + + b.Property("ScoreB") + .HasColumnType("integer"); + + b.Property("TeamSelectorA") + .IsRequired() + .HasColumnType("text"); + + b.Property("TeamSelectorB") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("TournamentId", "Id"); + + b.HasIndex("TournamentId", "GroupId"); + + b.ToTable("Matches", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.RankingOverwrite", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AssignTeamId") + .HasColumnType("integer"); + + b.Property("AssignTeamTournamentId") + .HasColumnType("bigint"); + + b.Property("HideRanking") + .HasColumnType("boolean"); + + b.Property("PlacementRank") + .HasColumnType("integer"); + + b.HasKey("TournamentId", "Id"); + + b.HasIndex("AssignTeamTournamentId", "AssignTeamId"); + + b.ToTable("RankingOverwrites", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.Property("TournamentId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("EntryFeePaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutOfCompetition") + .HasColumnType("boolean"); + + b.HasKey("TournamentId", "Id"); + + b.ToTable("Teams", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BannerImageId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FolderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PrimaryLogoId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("PublicPageViews") + .HasColumnType("integer"); + + b.Property("SecondaryLogoId") + .HasColumnType("bigint"); + + b.Property("VenueId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BannerImageId"); + + b.HasIndex("FolderId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PrimaryLogoId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("SecondaryLogoId"); + + b.HasIndex("VenueId"); + + b.ToTable("Tournaments", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("Contact") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactTelephone") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FormSession") + .HasColumnType("uuid"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("SourceLinkId") + .HasColumnType("bigint"); + + b.Property("Tag") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FormSession") + .IsUnique(); + + b.HasIndex("PlanningRealmId"); + + b.HasIndex("SourceLinkId"); + + b.ToTable("Applications", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.ApplicationChangeLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChangeLogs", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.ApplicationTeam", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("ClassId"); + + b.ToTable("ApplicationTeams", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.InvitationLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactPerson") + .HasColumnType("text"); + + b.Property("ContactTelephone") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.Property("PrimaryLogoId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("SecondaryLogoId") + .HasColumnType("bigint"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.HasIndex("PrimaryLogoId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("SecondaryLogoId"); + + b.ToTable("InvitationLinks", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.InvitationLinkEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowNewRegistrations") + .HasColumnType("boolean"); + + b.Property("ClassId") + .HasColumnType("bigint"); + + b.Property("InvitationLinkId") + .HasColumnType("bigint"); + + b.Property("MaxTeamsPerRegistration") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("InvitationLinkId"); + + b.ToTable("InvitationLinkEntries", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.Label", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("Labels", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TeamLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationTeamId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("TeamTournamentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationTeamId") + .IsUnique(); + + b.HasIndex("TeamTournamentId", "TeamId") + .IsUnique(); + + b.ToTable("TeamLinks", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TournamentClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningRealmId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PlanningRealmId"); + + b.ToTable("TournamentClasses", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TournamentPlanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("PlanningRealms", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowCreateOrganization") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsAdministrator") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChange") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEMail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrincipalId") + .HasColumnType("uuid"); + + b.Property("SecurityStamp") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEMail") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique(); + + b.HasIndex("PrincipalId") + .IsUnique(); + + b.ToTable("Users", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection>("AddressDetails") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection>("ExternalLinks") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("Venues", "turnierplan"); + }); + + modelBuilder.Entity("ApplicationTeamLabel", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.ApplicationTeam", null) + .WithMany() + .HasForeignKey("ApplicationTeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.TournamentPlanner.Label", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ResourceGroupResourcePlannerView", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourceGroup", null) + .WithMany() + .HasForeignKey("ResourceGroupsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlannerView", null) + .WithMany() + .HasForeignKey("ResourcePlannerViewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKeyRequest", b => + { + b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "ApiKey") + .WithMany("Requests") + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Turnierplan.Core.Document.Document", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Tournament") + .WithMany("Documents") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Folders") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Images") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.Resource", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "ResourcePlanner") + .WithMany() + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResourcePlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceAssignment", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourceGroup", "ResourceGroup") + .WithMany("ResourceAssignments") + .HasForeignKey("ResourceGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.ResourcePlanner.Resource", "Resource") + .WithMany() + .HasForeignKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resource"); + + b.Navigation("ResourceGroup"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceGroup", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "ResourcePlanner") + .WithMany("ResourceGroups") + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResourcePlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlanner", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("ResourcePlanners") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlannerView", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "ResourcePlanner") + .WithMany("ResourcePlannerViews") + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResourcePlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Folder.Folder", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Image.Image", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentPlanner", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.Venue.Venue", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("VenueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("Groups") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.GroupParticipant", b => + { + b.HasOne("Turnierplan.Core.Tournament.Group", "Group") + .WithMany("Participants") + .HasForeignKey("TournamentId", "GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "Team") + .WithMany() + .HasForeignKey("TournamentId", "TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Match", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("Matches") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Group", "Group") + .WithMany() + .HasForeignKey("TournamentId", "GroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.RankingOverwrite", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", null) + .WithMany("RankingOverwrites") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "AssignTeam") + .WithMany() + .HasForeignKey("AssignTeamTournamentId", "AssignTeamId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignTeam"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.HasOne("Turnierplan.Core.Tournament.Tournament", "Tournament") + .WithMany("Teams") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.HasOne("Turnierplan.Core.Image.Image", "BannerImage") + .WithMany() + .HasForeignKey("BannerImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Folder.Folder", "Folder") + .WithMany("Tournaments") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Tournaments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Image.Image", "PrimaryLogo") + .WithMany() + .HasForeignKey("PrimaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Image.Image", "SecondaryLogo") + .WithMany() + .HasForeignKey("SecondaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Venue.Venue", "Venue") + .WithMany("Tournaments") + .HasForeignKey("VenueId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("Turnierplan.Core.Tournament.ComputationConfiguration", "ComputationConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.PrimitiveCollection("ComparisonModes") + .IsRequired() + .HasJsonPropertyName("cmp"); + + b1.Property("HigherScoreLoses") + .HasJsonPropertyName("r"); + + b1.Property("MatchDrawnPoints") + .HasJsonPropertyName("d"); + + b1.Property("MatchLostPoints") + .HasJsonPropertyName("l"); + + b1.Property("MatchWonPoints") + .HasJsonPropertyName("w"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("ComputationConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + }); + + b.OwnsOne("Turnierplan.Core.Tournament.MatchPlanConfiguration", "MatchPlanConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("MatchPlanConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + + b1.OwnsOne("Turnierplan.Core.Tournament.FinalsRoundConfig", "FinalsRoundConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("EnableThirdPlacePlayoff") + .HasJsonPropertyName("3rd"); + + b2.Property("FirstFinalsRoundOrder") + .HasJsonPropertyName("fo"); + + b2.PrimitiveCollection("TeamSelectors") + .HasJsonPropertyName("ts"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("fr"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + + b2.OwnsMany("Turnierplan.Core.Tournament.AdditionalPlayoffConfig", "AdditionalPlayoffs", b3 => + { + b3.Property("FinalsRoundConfigMatchPlanConfigurationTournamentId"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b3.Property("PlayoffPosition") + .HasJsonPropertyName("p"); + + b3.Property("TeamSelectorA") + .IsRequired() + .HasJsonPropertyName("a"); + + b3.Property("TeamSelectorB") + .IsRequired() + .HasJsonPropertyName("b"); + + b3.HasKey("FinalsRoundConfigMatchPlanConfigurationTournamentId", "__synthesizedOrdinal"); + + b3.ToTable("Tournaments", "turnierplan"); + + b3.HasJsonPropertyName("ap"); + + b3.WithOwner() + .HasForeignKey("FinalsRoundConfigMatchPlanConfigurationTournamentId"); + }); + + b2.Navigation("AdditionalPlayoffs"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.GroupRoundConfig", "GroupRoundConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("GroupMatchOrder") + .HasJsonPropertyName("o"); + + b2.Property("GroupPhaseRounds") + .HasJsonPropertyName("r"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("gr"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.ScheduleConfig", "ScheduleConfig", b2 => + { + b2.Property("MatchPlanConfigurationTournamentId"); + + b2.Property("FinalsPhaseNumberOfCourts") + .HasJsonPropertyName("fc"); + + b2.Property("FinalsPhasePauseTime") + .HasJsonPropertyName("fp"); + + b2.Property("FinalsPhasePlayTime") + .HasJsonPropertyName("fd"); + + b2.Property("FirstMatchKickoff") + .HasJsonPropertyName("f"); + + b2.Property("GroupPhaseNumberOfCourts") + .HasJsonPropertyName("gc"); + + b2.Property("GroupPhasePauseTime") + .HasJsonPropertyName("gp"); + + b2.Property("GroupPhasePlayTime") + .HasJsonPropertyName("gd"); + + b2.Property("PauseBetweenGroupAndFinalsPhase") + .HasJsonPropertyName("p"); + + b2.HasKey("MatchPlanConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("sc"); + + b2.WithOwner() + .HasForeignKey("MatchPlanConfigurationTournamentId"); + }); + + b1.Navigation("FinalsRoundConfig"); + + b1.Navigation("GroupRoundConfig"); + + b1.Navigation("ScheduleConfig"); + }); + + b.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration", "PresentationConfiguration", b1 => + { + b1.Property("TournamentId"); + + b1.Property("ShowPrimaryLogo") + .HasJsonPropertyName("ol"); + + b1.Property("ShowResults") + .HasJsonPropertyName("o"); + + b1.Property("ShowSecondaryLogo") + .HasJsonPropertyName("sl"); + + b1.HasKey("TournamentId"); + + b1.ToTable("Tournaments", "turnierplan"); + + b1.ToJson("PresentationConfiguration"); + + b1.WithOwner() + .HasForeignKey("TournamentId"); + + b1.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration+HeaderLine", "Header1", b2 => + { + b2.Property("PresentationConfigurationTournamentId"); + + b2.Property("Content") + .HasJsonPropertyName("c"); + + b2.Property("CustomContent") + .HasJsonPropertyName("cc"); + + b2.HasKey("PresentationConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("h1"); + + b2.WithOwner() + .HasForeignKey("PresentationConfigurationTournamentId"); + }); + + b1.OwnsOne("Turnierplan.Core.Tournament.PresentationConfiguration+HeaderLine", "Header2", b2 => + { + b2.Property("PresentationConfigurationTournamentId"); + + b2.Property("Content") + .HasJsonPropertyName("c"); + + b2.Property("CustomContent") + .HasJsonPropertyName("cc"); + + b2.HasKey("PresentationConfigurationTournamentId"); + + b2.ToTable("Tournaments", "turnierplan"); + + b2.HasJsonPropertyName("h2"); + + b2.WithOwner() + .HasForeignKey("PresentationConfigurationTournamentId"); + }); + + b1.Navigation("Header1") + .IsRequired(); + + b1.Navigation("Header2") + .IsRequired(); + }); + + b.Navigation("BannerImage"); + + b.Navigation("ComputationConfiguration") + .IsRequired(); + + b.Navigation("Folder"); + + b.Navigation("MatchPlanConfiguration"); + + b.Navigation("Organization"); + + b.Navigation("PresentationConfiguration") + .IsRequired(); + + b.Navigation("PrimaryLogo"); + + b.Navigation("SecondaryLogo"); + + b.Navigation("Venue"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.Application", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentPlanner", "TournamentPlanner") + .WithMany("Applications") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.TournamentPlanner.InvitationLink", "SourceLink") + .WithMany() + .HasForeignKey("SourceLinkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("SourceLink"); + + b.Navigation("TournamentPlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.ApplicationChangeLog", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.Application", "Application") + .WithMany("ChangeLog") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Turnierplan.Core.TournamentPlanner.ApplicationChangeLog+Property", "Properties", b1 => + { + b1.Property("ApplicationChangeLogId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Type") + .HasJsonPropertyName("t"); + + b1.Property("Value") + .IsRequired() + .HasJsonPropertyName("v"); + + b1.HasKey("ApplicationChangeLogId", "__synthesizedOrdinal"); + + b1.ToTable("ApplicationChangeLogs", "turnierplan"); + + b1.ToJson("Properties"); + + b1.WithOwner() + .HasForeignKey("ApplicationChangeLogId"); + }); + + b.Navigation("Application"); + + b.Navigation("Properties"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.ApplicationTeam", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.Application", "Application") + .WithMany("Teams") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentClass", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.InvitationLink", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentPlanner", "TournamentPlanner") + .WithMany("InvitationLinks") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Image.Image", "PrimaryLogo") + .WithMany() + .HasForeignKey("PrimaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Turnierplan.Core.Image.Image", "SecondaryLogo") + .WithMany() + .HasForeignKey("SecondaryLogoId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsMany("Turnierplan.Core.TournamentPlanner.InvitationLink+ExternalLink", "ExternalLinks", b1 => + { + b1.Property("InvitationLinkId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Name") + .IsRequired() + .HasJsonPropertyName("n"); + + b1.Property("Url") + .IsRequired() + .HasJsonPropertyName("u"); + + b1.HasKey("InvitationLinkId", "__synthesizedOrdinal"); + + b1.ToTable("InvitationLinks", "turnierplan"); + + b1.ToJson("ExternalLinks"); + + b1.WithOwner() + .HasForeignKey("InvitationLinkId"); + }); + + b.Navigation("ExternalLinks"); + + b.Navigation("PrimaryLogo"); + + b.Navigation("SecondaryLogo"); + + b.Navigation("TournamentPlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.InvitationLinkEntry", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentClass", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.TournamentPlanner.InvitationLink", null) + .WithMany("Entries") + .HasForeignKey("InvitationLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.Label", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentPlanner", null) + .WithMany("Labels") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TeamLink", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.ApplicationTeam", "ApplicationTeam") + .WithOne("TeamLink") + .HasForeignKey("Turnierplan.Core.TournamentPlanner.TeamLink", "ApplicationTeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.Tournament.Team", "Team") + .WithOne("TeamLink") + .HasForeignKey("Turnierplan.Core.TournamentPlanner.TeamLink", "TeamTournamentId", "TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationTeam"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TournamentClass", b => + { + b.HasOne("Turnierplan.Core.TournamentPlanner.TournamentPlanner", null) + .WithMany("TournamentClasses") + .HasForeignKey("PlanningRealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TournamentPlanner", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("TournamentPlanners") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("Venues") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => + { + b.Navigation("Requests"); + + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Folder.Folder", b => + { + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Image.Image", b => + { + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Organization.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Folders"); + + b.Navigation("Images"); + + b.Navigation("ResourcePlanners"); + + b.Navigation("RoleAssignments"); + + b.Navigation("TournamentPlanners"); + + b.Navigation("Tournaments"); + + b.Navigation("Venues"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceGroup", b => + { + b.Navigation("ResourceAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlanner", b => + { + b.Navigation("ResourceGroups"); + + b.Navigation("ResourcePlannerViews"); + + b.Navigation("RoleAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => + { + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Team", b => + { + b.Navigation("TeamLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.Tournament.Tournament", b => + { + b.Navigation("Documents"); + + b.Navigation("Groups"); + + b.Navigation("Matches"); + + b.Navigation("RankingOverwrites"); + + b.Navigation("RoleAssignments"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.Application", b => + { + b.Navigation("ChangeLog"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.ApplicationTeam", b => + { + b.Navigation("TeamLink"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.InvitationLink", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("Turnierplan.Core.TournamentPlanner.TournamentPlanner", b => + { + b.Navigation("Applications"); + + b.Navigation("InvitationLinks"); + + b.Navigation("Labels"); + + b.Navigation("RoleAssignments"); + + b.Navigation("TournamentClasses"); + }); + + modelBuilder.Entity("Turnierplan.Core.Venue.Venue", b => + { + b.Navigation("RoleAssignments"); + + b.Navigation("Tournaments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs b/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs new file mode 100644 index 00000000..82ad7fc3 --- /dev/null +++ b/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs @@ -0,0 +1,273 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Turnierplan.Dal.Migrations +{ + /// + public partial class Add_ResourcePlanner : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ResourcePlanners", + schema: "turnierplan", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PublicId = table.Column(type: "bigint", nullable: false), + OrganizationId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResourcePlanners", x => x.Id); + table.ForeignKey( + name: "FK_ResourcePlanners_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalSchema: "turnierplan", + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "IAM_ResourcePlanner", + schema: "turnierplan", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ResourcePlannerId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Role = table.Column(type: "integer", nullable: false), + Principal = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IAM_ResourcePlanner", x => x.Id); + table.ForeignKey( + name: "FK_IAM_ResourcePlanner_ResourcePlanners_ResourcePlannerId", + column: x => x.ResourcePlannerId, + principalSchema: "turnierplan", + principalTable: "ResourcePlanners", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ResourceGroups", + schema: "turnierplan", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ResourcePlannerId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: true), + Description = table.Column(type: "text", nullable: true), + Type = table.Column(type: "integer", nullable: false), + Start = table.Column(type: "timestamp with time zone", nullable: true), + End = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ResourceGroups", x => x.Id); + table.ForeignKey( + name: "FK_ResourceGroups_ResourcePlanners_ResourcePlannerId", + column: x => x.ResourcePlannerId, + principalSchema: "turnierplan", + principalTable: "ResourcePlanners", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ResourcePlannerViews", + schema: "turnierplan", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PublicId = table.Column(type: "bigint", nullable: false), + ResourcePlannerId = table.Column(type: "bigint", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + DisplayAllGroups = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResourcePlannerViews", x => x.Id); + table.ForeignKey( + name: "FK_ResourcePlannerViews_ResourcePlanners_ResourcePlannerId", + column: x => x.ResourcePlannerId, + principalSchema: "turnierplan", + principalTable: "ResourcePlanners", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Resources", + schema: "turnierplan", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ResourcePlannerId = table.Column(type: "bigint", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: false), + Notes = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Resources", x => x.Id); + table.ForeignKey( + name: "FK_Resources_ResourcePlanners_ResourcePlannerId", + column: x => x.ResourcePlannerId, + principalSchema: "turnierplan", + principalTable: "ResourcePlanners", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ResourceGroupResourcePlannerView", + schema: "turnierplan", + columns: table => new + { + ResourceGroupsId = table.Column(type: "bigint", nullable: false), + ResourcePlannerViewId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResourceGroupResourcePlannerView", x => new { x.ResourceGroupsId, x.ResourcePlannerViewId }); + table.ForeignKey( + name: "FK_ResourceGroupResourcePlannerView_ResourceGroups_ResourceGro~", + column: x => x.ResourceGroupsId, + principalSchema: "turnierplan", + principalTable: "ResourceGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ResourceGroupResourcePlannerView_ResourcePlannerViews_Resou~", + column: x => x.ResourcePlannerViewId, + principalSchema: "turnierplan", + principalTable: "ResourcePlannerViews", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ResourceAssignments", + schema: "turnierplan", + columns: table => new + { + ResourceGroupId = table.Column(type: "bigint", nullable: false), + ResourceId = table.Column(type: "bigint", nullable: false), + State = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResourceAssignments", x => new { x.ResourceGroupId, x.ResourceId }); + table.ForeignKey( + name: "FK_ResourceAssignments_ResourceGroups_ResourceGroupId", + column: x => x.ResourceGroupId, + principalSchema: "turnierplan", + principalTable: "ResourceGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ResourceAssignments_Resources_ResourceId", + column: x => x.ResourceId, + principalSchema: "turnierplan", + principalTable: "Resources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_IAM_ResourcePlanner_ResourcePlannerId", + schema: "turnierplan", + table: "IAM_ResourcePlanner", + column: "ResourcePlannerId"); + + migrationBuilder.CreateIndex( + name: "IX_ResourceAssignments_ResourceId", + schema: "turnierplan", + table: "ResourceAssignments", + column: "ResourceId"); + + migrationBuilder.CreateIndex( + name: "IX_ResourceGroupResourcePlannerView_ResourcePlannerViewId", + schema: "turnierplan", + table: "ResourceGroupResourcePlannerView", + column: "ResourcePlannerViewId"); + + migrationBuilder.CreateIndex( + name: "IX_ResourceGroups_ResourcePlannerId", + schema: "turnierplan", + table: "ResourceGroups", + column: "ResourcePlannerId"); + + migrationBuilder.CreateIndex( + name: "IX_ResourcePlanners_OrganizationId", + schema: "turnierplan", + table: "ResourcePlanners", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_ResourcePlanners_PublicId", + schema: "turnierplan", + table: "ResourcePlanners", + column: "PublicId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ResourcePlannerViews_ResourcePlannerId", + schema: "turnierplan", + table: "ResourcePlannerViews", + column: "ResourcePlannerId"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_ResourcePlannerId", + schema: "turnierplan", + table: "Resources", + column: "ResourcePlannerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "IAM_ResourcePlanner", + schema: "turnierplan"); + + migrationBuilder.DropTable( + name: "ResourceAssignments", + schema: "turnierplan"); + + migrationBuilder.DropTable( + name: "ResourceGroupResourcePlannerView", + schema: "turnierplan"); + + migrationBuilder.DropTable( + name: "Resources", + schema: "turnierplan"); + + migrationBuilder.DropTable( + name: "ResourceGroups", + schema: "turnierplan"); + + migrationBuilder.DropTable( + name: "ResourcePlannerViews", + schema: "turnierplan"); + + migrationBuilder.DropTable( + name: "ResourcePlanners", + schema: "turnierplan"); + } + } +} diff --git a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs index 2eb7ba1d..1b592ec7 100644 --- a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs +++ b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs @@ -38,6 +38,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApplicationTeamLabel", "turnierplan"); }); + modelBuilder.Entity("ResourceGroupResourcePlannerView", b => + { + b.Property("ResourceGroupsId") + .HasColumnType("bigint"); + + b.Property("ResourcePlannerViewId") + .HasColumnType("bigint"); + + b.HasKey("ResourceGroupsId", "ResourcePlannerViewId"); + + b.HasIndex("ResourcePlannerViewId"); + + b.ToTable("ResourceGroupResourcePlannerView", "turnierplan"); + }); + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => { b.Property("Id") @@ -270,6 +285,143 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Organizations", "turnierplan"); }); + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.Resource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("Resources", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceAssignment", b => + { + b.Property("ResourceGroupId") + .HasColumnType("bigint"); + + b.Property("ResourceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("ResourceGroupId", "ResourceId"); + + b.HasIndex("ResourceId"); + + b.ToTable("ResourceAssignments", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("ResourceGroups", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("bigint"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("ResourcePlanners", "turnierplan"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlannerView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayAllGroups") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("PublicId") + .HasColumnType("bigint"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("ResourcePlannerViews", "turnierplan"); + }); + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { b.Property("Id") @@ -374,6 +526,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("IAM_Organization", "turnierplan"); }); + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Principal") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResourcePlannerId") + .HasColumnType("bigint"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ResourcePlannerId"); + + b.ToTable("IAM_ResourcePlanner", "turnierplan"); + }); + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { b.Property("Id") @@ -1089,6 +1267,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("ResourceGroupResourcePlannerView", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourceGroup", null) + .WithMany() + .HasForeignKey("ResourceGroupsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlannerView", null) + .WithMany() + .HasForeignKey("ResourcePlannerViewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Turnierplan.Core.ApiKey.ApiKey", b => { b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") @@ -1144,6 +1337,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Organization"); }); + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.Resource", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "ResourcePlanner") + .WithMany() + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResourcePlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceAssignment", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourceGroup", "ResourceGroup") + .WithMany("ResourceAssignments") + .HasForeignKey("ResourceGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Turnierplan.Core.ResourcePlanner.Resource", "Resource") + .WithMany() + .HasForeignKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resource"); + + b.Navigation("ResourceGroup"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceGroup", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "ResourcePlanner") + .WithMany("ResourceGroups") + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResourcePlanner"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlanner", b => + { + b.HasOne("Turnierplan.Core.Organization.Organization", "Organization") + .WithMany("ResourcePlanners") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlannerView", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "ResourcePlanner") + .WithMany("ResourcePlannerViews") + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ResourcePlanner"); + }); + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { b.HasOne("Turnierplan.Core.ApiKey.ApiKey", "Scope") @@ -1188,6 +1444,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Scope"); }); + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => + { + b.HasOne("Turnierplan.Core.ResourcePlanner.ResourcePlanner", "Scope") + .WithMany("RoleAssignments") + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { b.HasOne("Turnierplan.Core.Tournament.Tournament", "Scope") @@ -1805,6 +2072,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Images"); + b.Navigation("ResourcePlanners"); + b.Navigation("RoleAssignments"); b.Navigation("TournamentPlanners"); @@ -1814,6 +2083,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Venues"); }); + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourceGroup", b => + { + b.Navigation("ResourceAssignments"); + }); + + modelBuilder.Entity("Turnierplan.Core.ResourcePlanner.ResourcePlanner", b => + { + b.Navigation("ResourceGroups"); + + b.Navigation("ResourcePlannerViews"); + + b.Navigation("RoleAssignments"); + }); + modelBuilder.Entity("Turnierplan.Core.Tournament.Group", b => { b.Navigation("Participants"); From 7aa0d201840c44438e530df3b49f56c7ff4ddb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 10:23:06 +0200 Subject: [PATCH 09/18] Add repository & adjust access validator --- src/Turnierplan.App/Helpers/RbacScopeHelper.cs | 2 +- src/Turnierplan.App/Security/AccessValidator.cs | 5 +++++ .../Extensions/ServiceCollectionExtensions.cs | 5 +++++ .../Repositories/ResourcePlannerRepository.cs | 11 +++++++++++ .../Repositories/RoleAssignmentRepository.cs | 3 +++ 5 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs diff --git a/src/Turnierplan.App/Helpers/RbacScopeHelper.cs b/src/Turnierplan.App/Helpers/RbacScopeHelper.cs index 02b1f14b..8771ae30 100644 --- a/src/Turnierplan.App/Helpers/RbacScopeHelper.cs +++ b/src/Turnierplan.App/Helpers/RbacScopeHelper.cs @@ -30,6 +30,6 @@ public static bool TryParseScopeId(string scopeId, [NotNullWhen(true)] out strin return true; } - [GeneratedRegex("^(?ApiKey|Folder|Image|Organization|Tournament|TournamentPlanner|Venue):(?[A-Za-z0-9_-]{11})$")] + [GeneratedRegex("^(?ApiKey|Folder|Image|Organization|ResourcePlanner|Tournament|TournamentPlanner|Venue):(?[A-Za-z0-9_-]{11})$")] public static partial Regex ScopeIdRegex(); } diff --git a/src/Turnierplan.App/Security/AccessValidator.cs b/src/Turnierplan.App/Security/AccessValidator.cs index b176106b..387cf347 100644 --- a/src/Turnierplan.App/Security/AccessValidator.cs +++ b/src/Turnierplan.App/Security/AccessValidator.cs @@ -4,6 +4,7 @@ using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.PublicId; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; @@ -103,6 +104,7 @@ internal static bool IsActionAllowed(IEntityWithRoleAssignments target, Ac ApiKey apiKey => IsActionAllowed(apiKey.Organization, action, principal), Image image => IsActionAllowed(image.Organization, action, principal), Folder folder => IsActionAllowed(folder.Organization, action, principal), + ResourcePlanner resourcePlanner => IsActionAllowed(resourcePlanner.Organization, action, principal), TournamentPlanner tournamentPlanner => IsActionAllowed(tournamentPlanner.Organization, action, principal), Tournament tournament => (tournament.Folder is not null && IsActionAllowed(tournament.Folder, action, principal)) || IsActionAllowed(tournament.Organization, action, principal), Venue venue => IsActionAllowed(venue.Organization, action, principal), @@ -126,6 +128,9 @@ internal static void AddAvailableRoles(IEntityWithRoleAssignments target, case Folder folder: AddAvailableRoles(folder.Organization, rolesList, principal); break; + case ResourcePlanner resourcePlanner: + AddAvailableRoles(resourcePlanner.Organization, rolesList, principal); + break; case TournamentPlanner tournamentPlanner: AddAvailableRoles(tournamentPlanner.Organization, rolesList, principal); break; diff --git a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs index 9f0d3b7b..5f41a85a 100644 --- a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; using Turnierplan.Core.Venue; @@ -55,6 +56,7 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -66,6 +68,7 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped>(sp => sp.GetRequiredService()); services.AddScoped>(sp => sp.GetRequiredService()); services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); services.AddScoped>(sp => sp.GetRequiredService()); services.AddScoped>(sp => sp.GetRequiredService()); services.AddScoped>(sp => sp.GetRequiredService()); @@ -74,6 +77,7 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped, FolderRoleAssignmentRepository>(); services.AddScoped, ImageRoleAssignmentRepository>(); services.AddScoped, OrganizationRoleAssignmentRepository>(); + services.AddScoped, ResourcePlannerRoleAssignmentRepository>(); services.AddScoped, TournamentRoleAssignmentRepository>(); services.AddScoped, TournamentPlannerRoleAssignmentRepository>(); services.AddScoped, VenueRoleAssignmentRepository>(); @@ -82,6 +86,7 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped(sp => sp.GetRequiredService>()); services.AddScoped(sp => sp.GetRequiredService>()); services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); services.AddScoped(sp => sp.GetRequiredService>()); services.AddScoped(sp => sp.GetRequiredService>()); services.AddScoped(sp => sp.GetRequiredService>()); diff --git a/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs new file mode 100644 index 00000000..e3270c83 --- /dev/null +++ b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs @@ -0,0 +1,11 @@ +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.Dal.Repositories; + +public interface IResourcePlannerRepository : IRepositoryWithPublicId +{ +} + +internal sealed class ResourcePlannerRepository(TurnierplanContext context) : RepositoryBaseWithPublicId(context), IResourcePlannerRepository +{ +} diff --git a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs index 9264834b..f41e131b 100644 --- a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs +++ b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs @@ -4,6 +4,7 @@ using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; @@ -37,6 +38,8 @@ internal sealed class ImageRoleAssignmentRepository(TurnierplanContext context) internal sealed class OrganizationRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context); +internal sealed class ResourcePlannerRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context); + internal sealed class TournamentPlannerRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context); internal sealed class TournamentRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context); From c7321490828e4b97a7315c50dbf80d65ee67abc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 10:29:01 +0200 Subject: [PATCH 10/18] Add new endpoints for create & list resource planners --- .../CreateResourcePlannerEndpoint.cs | 73 +++++++++++++++++++ .../GetResourcePlannersEndpoint.cs | 38 ++++++++++ .../Rules/ResourcePlannerHeaderMappingRule.cs | 17 +++++ .../Rules/ResourcePlannerMappingRule.cs | 21 ++++++ .../Models/ResourcePlannerDto.cs | 26 +++++++ .../Repositories/OrganizationRepository.cs | 8 +- 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/Turnierplan.App/Endpoints/ResourcePlanners/CreateResourcePlannerEndpoint.cs create mode 100644 src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannersEndpoint.cs create mode 100644 src/Turnierplan.App/Mapping/Rules/ResourcePlannerHeaderMappingRule.cs create mode 100644 src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs create mode 100644 src/Turnierplan.App/Models/ResourcePlannerDto.cs diff --git a/src/Turnierplan.App/Endpoints/ResourcePlanners/CreateResourcePlannerEndpoint.cs b/src/Turnierplan.App/Endpoints/ResourcePlanners/CreateResourcePlannerEndpoint.cs new file mode 100644 index 00000000..30023e97 --- /dev/null +++ b/src/Turnierplan.App/Endpoints/ResourcePlanners/CreateResourcePlannerEndpoint.cs @@ -0,0 +1,73 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Turnierplan.App.Extensions; +using Turnierplan.App.Mapping; +using Turnierplan.App.Models; +using Turnierplan.App.Security; +using Turnierplan.Core.PublicId; +using Turnierplan.Core.ResourcePlanner; +using Turnierplan.Dal.Repositories; + +namespace Turnierplan.App.Endpoints.ResourcePlanners; + +internal sealed class CreateResourcePlannerEndpoint : EndpointBase +{ + protected override HttpMethod Method => HttpMethod.Post; + + protected override string Route => "/api/resource-planners"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromBody] CreateResourcePlannerEndpointRequest request, + IOrganizationRepository organizationRepository, + IAccessValidator accessValidator, + IResourcePlannerRepository resourcePlannerRepository, + IMapper mapper, + CancellationToken cancellationToken) + { + if (!Validator.Instance.ValidateAndGetResult(request, out var result)) + { + return result; + } + + var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId); + + if (organization is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(organization, Actions.GenericWrite)) + { + return Results.Forbid(); + } + + var resourcePlanner = new ResourcePlanner(organization, request.Name.Trim()); + + await resourcePlannerRepository.CreateAsync(resourcePlanner); + await resourcePlannerRepository.UnitOfWork.SaveChangesAsync(cancellationToken); + + accessValidator.AddRolesToResponseHeader(resourcePlanner); + + return Results.Ok(mapper.Map(resourcePlanner)); + } + + public sealed record CreateResourcePlannerEndpointRequest + { + public required PublicId OrganizationId { get; init; } + + public required string Name { get; init; } + } + + internal sealed class Validator : AbstractValidator + { + public static readonly Validator Instance = new(); + + private Validator() + { + RuleFor(x => x.Name) + .NotEmpty(); + } + } +} diff --git a/src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannersEndpoint.cs b/src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannersEndpoint.cs new file mode 100644 index 00000000..bdf1b538 --- /dev/null +++ b/src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannersEndpoint.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Turnierplan.App.Mapping; +using Turnierplan.App.Models; +using Turnierplan.App.Security; +using Turnierplan.Core.PublicId; +using Turnierplan.Dal.Repositories; + +namespace Turnierplan.App.Endpoints.ResourcePlanners; + +internal sealed class GetResourcePlannersEndpoint : EndpointBase> +{ + protected override HttpMethod Method => HttpMethod.Get; + + protected override string Route => "/api/resource-planners"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromQuery] PublicId organizationId, + IOrganizationRepository organizationRepository, + IAccessValidator accessValidator, + IMapper mapper) + { + var organization = await organizationRepository.GetByPublicIdAsync(organizationId.Value, IOrganizationRepository.Includes.ResourcePlanners); + + if (organization is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(organization, Actions.GenericRead)) + { + return Results.Forbid(); + } + + return Results.Ok(mapper.MapCollection(organization.ResourcePlanners)); + } +} diff --git a/src/Turnierplan.App/Mapping/Rules/ResourcePlannerHeaderMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerHeaderMappingRule.cs new file mode 100644 index 00000000..ecb57e52 --- /dev/null +++ b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerHeaderMappingRule.cs @@ -0,0 +1,17 @@ +using Turnierplan.App.Models; +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.App.Mapping.Rules; + +internal sealed class ResourcePlannerHeaderMappingRule : MappingRuleBase +{ + protected override ResourcePlannerHeaderDto Map(IMapper mapper, MappingContext context, ResourcePlanner source) + { + return new ResourcePlannerHeaderDto + { + Id = source.PublicId, + OrganizationId = source.Organization.PublicId, + Name = source.Name + }; + } +} diff --git a/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs new file mode 100644 index 00000000..34824127 --- /dev/null +++ b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs @@ -0,0 +1,21 @@ +using Turnierplan.App.Helpers; +using Turnierplan.App.Models; +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.App.Mapping.Rules; + +internal sealed class ResourcePlannerMappingRule : MappingRuleBase +{ + protected override ResourcePlannerDto Map(IMapper mapper, MappingContext context, ResourcePlanner source) + { + return new ResourcePlannerDto + { + Id = source.PublicId, + OrganizationId = source.Organization.PublicId, + RbacScopeId = source.GetScopeId(), + Name = source.Name, + // groups + // views + }; + } +} diff --git a/src/Turnierplan.App/Models/ResourcePlannerDto.cs b/src/Turnierplan.App/Models/ResourcePlannerDto.cs new file mode 100644 index 00000000..f077b307 --- /dev/null +++ b/src/Turnierplan.App/Models/ResourcePlannerDto.cs @@ -0,0 +1,26 @@ +using Turnierplan.Core.PublicId; + +namespace Turnierplan.App.Models; + +public sealed record ResourcePlannerDto +{ + public required PublicId Id { get; init; } + + public required PublicId OrganizationId { get; init; } + + public required string RbacScopeId { get; init; } + + public required string Name { get; init; } + + // groups + // views +} + +public sealed record ResourcePlannerHeaderDto +{ + public required PublicId Id { get; init; } + + public required PublicId OrganizationId { get; init; } + + public required string Name { get; init; } +} diff --git a/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs b/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs index d3d314f4..06b3e10d 100644 --- a/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs +++ b/src/Turnierplan.Dal/Repositories/OrganizationRepository.cs @@ -25,7 +25,8 @@ public enum Includes Venues = 4, Images = 8, ApiKeys = 16, - TournamentPlanners = 32 + TournamentPlanners = 32, + ResourcePlanners = 64 } } @@ -74,6 +75,11 @@ internal sealed class OrganizationRepository(TurnierplanContext context) : Repos query = query.Include(x => x.TournamentPlanners); } + if (includes.HasFlag(IOrganizationRepository.Includes.ResourcePlanners)) + { + query = query.Include(x => x.ResourcePlanners); + } + query = query .Include(x => x.RoleAssignments) .AsSplitQuery(); From f59d6621a8165a5dcd2559cea53d5eff8bbcbb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 10:39:36 +0200 Subject: [PATCH 11/18] Add missing DbSets --- src/Turnierplan.Dal/TurnierplanContext.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Turnierplan.Dal/TurnierplanContext.cs b/src/Turnierplan.Dal/TurnierplanContext.cs index b2d277a0..1a1d4471 100644 --- a/src/Turnierplan.Dal/TurnierplanContext.cs +++ b/src/Turnierplan.Dal/TurnierplanContext.cs @@ -67,6 +67,18 @@ public TurnierplanContext(DbContextOptions options, ILogger< public DbSet Organizations { get; set; } = null!; + public DbSet ResourceAssignments { get; set; } = null!; + + public DbSet Resources { get; set; } = null!; + + public DbSet ResourceGroups { get; set; } = null!; + + public DbSet ResourcePlanners { get; set; } = null!; + + public DbSet> ResourcePlannerRoleAssignments { get; set; } = null!; + + public DbSet ResourcePlannerViews { get; set; } = null!; + public DbSet> OrganizationRoleAssignments { get; set; } = null!; public DbSet TournamentPlanners { get; set; } = null!; From ed865697979f326212f314d6ec5ef332faf88936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 10:41:48 +0200 Subject: [PATCH 12/18] Add basic frontend integration with create page --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 16 +++ .../create-resource-planner.component.html | 36 +++++++ .../create-resource-planner.component.ts | 99 +++++++++++++++++++ .../view-organization.component.html | 40 ++++++++ .../view-organization.component.ts | 24 +++++ .../view-resource-planner.component.html | 1 + .../view-resource-planner.component.ts | 7 ++ .../Client/src/app/portal/portal.routes.ts | 11 +++ 8 files changed, 234 insertions(+) create mode 100644 src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.html create mode 100644 src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.ts create mode 100644 src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html create mode 100644 src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index a3f4b0fc..d293a036 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -172,6 +172,7 @@ export const de = { Tournaments: 'Turniere', Venues: 'Spielstätten', TournamentPlanners: 'Turnierplaner', + ResourcePlanners: 'Helferlisten', Images: 'Bilder', ApiKeys: 'API-Schlüssel', Settings: 'Einstellungen' @@ -180,6 +181,7 @@ export const de = { TournamentCount: 'Turniere', VenueCount: 'Spielstätten', TournamentPlannerCount: 'Turnierplaner', + ResourcePlannerCount: 'Helferlisten', ImagesCount: 'Bilder', ImagesTotalSize: 'Gesamtgröße', ApiKeyCount: 'API-Schlüssel' @@ -187,6 +189,7 @@ export const de = { NewTournament: 'Neues Turnier', NewVenue: 'Neue Spielstätte', NewTournamentPlanner: 'Neuer Turnierplaner', + NewResourcePlanner: 'Neue Helferliste', UploadImage: 'Bild hochladen', NewApiKey: 'Neuer API-Schlüssel', NoTournaments: 'In dieser Organisation gibt es aktuell keine Turniere.\nErstellen Sie ein Turner mit der Schaltfläche oben rechts.', @@ -194,7 +197,10 @@ export const de = { 'In dieser Organisation gibt es aktuell keine Spielstätten.\nErstellen Sie eine Spielstätte mit der Schaltfläche oben rechts.', NoTournamentPlanners: 'In dieser Organisation gibt es aktuell keine Turnierplaner.\nErstellen Sie einen Turnierplaner mit der Schaltfläche oben rechts.', + NoResourcePlanners: + 'In dieser Organisation gibt es aktuell keine Helferlisten.\nErstellen Sie eine Helferliste mit der Schaltfläche oben rechts.', OpenTournamentPlanner: 'öffnen', + OpenResourcePlanner: 'öffnen', TournamentExplorer: { EmptyFolder: 'In diesem Ordner befinden sich keine Turniere. Wählen Sie einen anderen Ordner oder erstellen Sie ein Turnier.', RenameFolder: { @@ -988,6 +994,16 @@ export const de = { OrganizationNotice: 'Es wird ein neuer Turnierplaner in der Organisation {{organizationName}} angelegt.', Submit: 'Erstellen' }, + CreateResourcePlanner: { + Title: 'Neue Helferliste erstellen', + Form: { + Name: 'Name', + NameInvalid: 'Der Name einer neuen Helferliste darf nicht leer sein.', + NameValid: 'Dieser Name kann verwendet werden.' + }, + OrganizationNotice: 'Es wird eine neue Helferliste in der Organisation {{organizationName}} angelegt.', + Submit: 'Erstellen' + }, ViewTournamentPlanner: { Pages: { TournamentClasses: 'Turnierklassen', diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.html new file mode 100644 index 00000000..bcbb0f55 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.html @@ -0,0 +1,36 @@ + + + +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.ts new file mode 100644 index 00000000..bdb5a615 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/create-resource-planner/create-resource-planner.component.ts @@ -0,0 +1,99 @@ +import { Component, OnDestroy } from '@angular/core'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { from, of, Subject, switchMap, takeUntil } from 'rxjs'; + +import { LoadingState, LoadingStateDirective } from '../../directives/loading-state.directive'; +import { TitleService } from '../../services/title.service'; +import { PageFrameComponent } from '../../components/page-frame/page-frame.component'; +import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'; +import { NgClass } from '@angular/common'; +import { ActionButtonComponent } from '../../components/action-button/action-button.component'; +import { OrganizationDto } from '../../../api/models/organization-dto'; +import { TurnierplanApi } from '../../../api/turnierplan-api'; +import { getOrganization } from '../../../api/fn/organizations/get-organization'; +import { createResourcePlanner } from '../../../api/fn/resource-planners/create-resource-planner'; + +@Component({ + templateUrl: './create-resource-planner.component.html', + imports: [ + LoadingStateDirective, + PageFrameComponent, + TranslateDirective, + FormsModule, + ReactiveFormsModule, + NgClass, + ActionButtonComponent, + TranslatePipe + ] +}) +export class CreateResourcePlannerComponent implements OnDestroy { + protected loadingState: LoadingState = { isLoading: false }; + + protected organization?: OrganizationDto; + protected resourcePlannerName = new FormControl('', { nonNullable: true }); + + private readonly destroyed$ = new Subject(); + + constructor( + private readonly turnierplanApi: TurnierplanApi, + private readonly route: ActivatedRoute, + private readonly router: Router, + private readonly titleService: TitleService + ) { + this.route.paramMap + .pipe( + takeUntil(this.destroyed$), + switchMap((params) => { + const organizationId = params.get('id'); + if (organizationId === null) { + this.loadingState = { isLoading: false }; + return of(undefined); + } + this.loadingState = { isLoading: true }; + return this.turnierplanApi.invoke(getOrganization, { id: organizationId }); + }) + ) + .subscribe({ + next: (result) => { + this.organization = result; + this.titleService.setTitleFrom(result); + this.loadingState = { isLoading: false }; + }, + error: (error) => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + + public ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + protected confirmButtonClicked(): void { + if (this.resourcePlannerName.valid && !this.loadingState.isLoading && this.organization) { + this.loadingState = { isLoading: true }; + this.turnierplanApi + .invoke(createResourcePlanner, { + body: { + organizationId: this.organization.id, + name: this.resourcePlannerName.value + } + }) + .pipe( + switchMap((resourcePlanner) => + from(this.router.navigate(['../../../../resource-planner/', resourcePlanner.id], { relativeTo: this.route })) + ) + ) + .subscribe({ + next: () => { + this.loadingState = { isLoading: false }; + }, + error: (error) => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html index 20bc9832..92dba3b5 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html @@ -42,6 +42,14 @@ [icon]="'upload'" [routerLink]="'upload/image'" /> } + @case (6) { + + } @case (2) { } } + @case (6) { + @if (isLoadingResourcePlanners || !resourcePlanners) { + + } @else { +
+ +
+
+ @if (resourcePlanners.length === 0) { +
+ +
+ } @else { +
+ @for (resourcePlanner of resourcePlanners; track resourcePlanner.id) { +
+
+
{{ resourcePlanner.name }}
+
+ +
+ } +
+ } +
+ } + } @case (2) { @if (isLoadingApiKeys || !apiKeys) { diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts index c3d78356..16f6cfca 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.ts @@ -50,6 +50,8 @@ import { NgbOffcanvas, NgbOffcanvasRef, NgbTooltip } from '@ng-bootstrap/ng-boot import { ApiKeyExtendComponent } from '../../components/api-key-extend/api-key-extend.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { filter } from 'rxjs/operators'; +import { ResourcePlannerHeaderDto } from '../../../api/models/resource-planner-header-dto'; +import { getResourcePlanners } from '../../../api/fn/resource-planners/get-resource-planners'; @Component({ templateUrl: './view-organization.component.html', @@ -89,6 +91,7 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { private static readonly venuesPageId = 1; private static readonly apiKeysPageId = 2; private static readonly tournamentPlannersPageId = 4; + private static readonly resourcePlannersPageId = 6; protected readonly Actions = Actions; @@ -97,12 +100,14 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { protected tournaments?: TournamentHeaderDto[]; protected venues?: VenueDto[]; protected tournamentPlanners?: TournamentPlannerHeaderDto[]; + protected resourcePlanners?: ResourcePlannerHeaderDto[]; protected images?: GetImagesEndpointResponse; protected imagesTotalSize?: number; protected apiKeys?: ApiKeyDto[]; protected displayApiKeyUsage?: string; protected isLoadingVenues = false; protected isLoadingTournamentPlanners = false; + protected isLoadingResourcePlanners = false; protected isLoadingImages = false; protected isLoadingApiKeys = false; @@ -125,6 +130,11 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { title: 'Portal.ViewOrganization.Pages.TournamentPlanners', icon: 'bi-ticket-perforated' }, + { + id: ViewOrganizationComponent.resourcePlannersPageId, + title: 'Portal.ViewOrganization.Pages.ResourcePlanners', + icon: 'bi-file-earmark-spreadsheet' + }, { id: ViewOrganizationComponent.imagesPageId, title: 'Portal.ViewOrganization.Pages.Images', @@ -244,6 +254,20 @@ export class ViewOrganizationComponent implements OnInit, OnDestroy { }); } + if (number === ViewOrganizationComponent.resourcePlannersPageId && !this.resourcePlanners && !this.isLoadingResourcePlanners) { + // Load resource planners only when the page is opened + this.isLoadingResourcePlanners = true; + this.turnierplanApi.invoke(getResourcePlanners, { organizationId: this.organization.id }).subscribe({ + next: (resourcePlanner) => { + this.resourcePlanners = resourcePlanner; + this.isLoadingResourcePlanners = false; + }, + error: (error) => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + if (number === ViewOrganizationComponent.imagesPageId && !this.images && !this.isLoadingImages) { // Load images only when the page is opened this.isLoadingImages = true; diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html new file mode 100644 index 00000000..71d2f849 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html @@ -0,0 +1 @@ +

view-resource-planner works!

diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts new file mode 100644 index 00000000..861f3cc8 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + imports: [], + templateUrl: './view-resource-planner.component.html' +}) +export class ViewResourcePlannerComponent {} diff --git a/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts b/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts index 99e5713e..834a4d86 100644 --- a/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts +++ b/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts @@ -19,6 +19,8 @@ import { ConfigureTournamentComponent } from './pages/configure-tournament/confi import { EditMatchPlanComponent } from './pages/edit-match-plan/edit-match-plan.component'; import { ViewVenueComponent } from './pages/view-venue/view-venue.component'; import { UploadImageComponent } from './pages/upload-image/upload-image.component'; +import { CreateResourcePlannerComponent } from './pages/create-resource-planner/create-resource-planner.component'; +import { ViewResourcePlannerComponent } from './pages/view-resource-planner/view-resource-planner.component'; export const portalRoutes: Routes = [ { @@ -61,6 +63,10 @@ export const portalRoutes: Routes = [ return `organization/${data.params['id']}/create/tournament-planner`; } }, + { + path: 'organization/:id/create/resource-planner', + component: CreateResourcePlannerComponent + }, { path: 'organization/:id/create/tournament-planner', component: CreateTournamentPlannerComponent @@ -79,6 +85,11 @@ export const portalRoutes: Routes = [ return `tournament-planner/${data.params['id']}`; } }, + { + path: 'resource-planner/:id', + component: ViewResourcePlannerComponent, + canDeactivate: [discardChangesGuard] + }, { path: 'tournament-planner/:id', component: ViewTournamentPlannerComponent, From e6195d946decfbb96baf2140f5048377f8f49afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 11:04:21 +0200 Subject: [PATCH 13/18] Add dtos, mapping & GET endpoint --- .../GetResourcePlannerEndpoint.cs | 40 +++++++++++++++ .../Rules/ResourcePlannerMappingRule.cs | 50 ++++++++++++++++++- .../Models/ResourceAssignmentDto.cs | 10 ++++ src/Turnierplan.App/Models/ResourceDto.cs | 14 ++++++ .../Models/ResourceGroupDto.cs | 20 ++++++++ .../Models/ResourcePlannerDto.cs | 12 ++--- .../Models/ResourcePlannerHeaderDto.cs | 12 +++++ .../Models/ResourcePlannerViewDto.cs | 16 ++++++ .../Repositories/ResourcePlannerRepository.cs | 34 +++++++++++++ 9 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannerEndpoint.cs create mode 100644 src/Turnierplan.App/Models/ResourceAssignmentDto.cs create mode 100644 src/Turnierplan.App/Models/ResourceDto.cs create mode 100644 src/Turnierplan.App/Models/ResourceGroupDto.cs create mode 100644 src/Turnierplan.App/Models/ResourcePlannerHeaderDto.cs create mode 100644 src/Turnierplan.App/Models/ResourcePlannerViewDto.cs diff --git a/src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannerEndpoint.cs b/src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannerEndpoint.cs new file mode 100644 index 00000000..67a9282d --- /dev/null +++ b/src/Turnierplan.App/Endpoints/ResourcePlanners/GetResourcePlannerEndpoint.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using Turnierplan.App.Mapping; +using Turnierplan.App.Models; +using Turnierplan.App.Security; +using Turnierplan.Core.PublicId; +using Turnierplan.Dal.Repositories; + +namespace Turnierplan.App.Endpoints.ResourcePlanners; + +internal sealed class GetResourcePlannerEndpoint : EndpointBase +{ + protected override HttpMethod Method => HttpMethod.Get; + + protected override string Route => "/api/resource-planners/{id}"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromRoute] PublicId id, + IResourcePlannerRepository repository, + IAccessValidator accessValidator, + IMapper mapper) + { + var resourcePlanner = await repository.GetByPublicIdAsync(id, IResourcePlannerRepository.Includes.All); + + if (resourcePlanner is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(resourcePlanner, Actions.GenericRead)) + { + return Results.Forbid(); + } + + accessValidator.AddRolesToResponseHeader(resourcePlanner); + + return Results.Ok(mapper.Map(resourcePlanner)); + } +} diff --git a/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs index 34824127..cc9e1ffb 100644 --- a/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs @@ -14,8 +14,54 @@ protected override ResourcePlannerDto Map(IMapper mapper, MappingContext context OrganizationId = source.Organization.PublicId, RbacScopeId = source.GetScopeId(), Name = source.Name, - // groups - // views + Resources = + [ + ..source.ResourceGroups + .SelectMany(group => group.ResourceAssignments) + .Select(assignment => assignment.Resource) + .Distinct() + .Select(resource => new ResourceDto + { + Id = resource.Id, + Type = resource.Type, + Name = resource.Name, + Notes = resource.Notes + }) + ], + ResourceGroups = + [ + ..source.ResourceGroups.Select(group => new ResourceGroupDto + { + Id = group.Id, + Name = group.Name, + Description = group.Description, + Type = group.Type, + Start = group.Start, + End = group.End, + Assignment = + [ + ..group.ResourceAssignments.Select(assignment => new ResourceAssignmentDto + { + ResourceId = assignment.Resource.Id, + State = assignment.State + }) + ] + }) + ], + Views = + [ + ..source.ResourcePlannerViews.Select(view => new ResourcePlannerViewDto + { + Id = view.Id, + PublicId = view.PublicId, + IsActive = view.IsActive, + DisplayAllGroups = view.DisplayAllGroups, + ResourceGroupIds = + [ + ..view.ResourceGroups.Select(group => group.Id) + ] + }) + ] }; } } diff --git a/src/Turnierplan.App/Models/ResourceAssignmentDto.cs b/src/Turnierplan.App/Models/ResourceAssignmentDto.cs new file mode 100644 index 00000000..c7c05251 --- /dev/null +++ b/src/Turnierplan.App/Models/ResourceAssignmentDto.cs @@ -0,0 +1,10 @@ +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.App.Models; + +public sealed record ResourceAssignmentDto +{ + public required long ResourceId { get; init; } + + public required ResourceAssignmentState State { get; init; } +} \ No newline at end of file diff --git a/src/Turnierplan.App/Models/ResourceDto.cs b/src/Turnierplan.App/Models/ResourceDto.cs new file mode 100644 index 00000000..d427c1b5 --- /dev/null +++ b/src/Turnierplan.App/Models/ResourceDto.cs @@ -0,0 +1,14 @@ +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.App.Models; + +public sealed record ResourceDto +{ + public required long Id { get; init; } + + public required ResourceType Type { get; init; } + + public required string Name { get; init; } + + public required string? Notes { get; init; } +} \ No newline at end of file diff --git a/src/Turnierplan.App/Models/ResourceGroupDto.cs b/src/Turnierplan.App/Models/ResourceGroupDto.cs new file mode 100644 index 00000000..9d9ab26f --- /dev/null +++ b/src/Turnierplan.App/Models/ResourceGroupDto.cs @@ -0,0 +1,20 @@ +using Turnierplan.Core.ResourcePlanner; + +namespace Turnierplan.App.Models; + +public sealed record ResourceGroupDto +{ + public required long Id { get; init; } + + public required string? Name { get; init; } + + public required string? Description { get; init; } + + public required ResourceGroupType Type { get; init; } + + public required DateTime? Start { get; init; } + + public required DateTime? End { get; init; } + + public required ResourceAssignmentDto[] Assignment { get; init; } +} diff --git a/src/Turnierplan.App/Models/ResourcePlannerDto.cs b/src/Turnierplan.App/Models/ResourcePlannerDto.cs index f077b307..5dff3fa0 100644 --- a/src/Turnierplan.App/Models/ResourcePlannerDto.cs +++ b/src/Turnierplan.App/Models/ResourcePlannerDto.cs @@ -12,15 +12,9 @@ public sealed record ResourcePlannerDto public required string Name { get; init; } - // groups - // views -} - -public sealed record ResourcePlannerHeaderDto -{ - public required PublicId Id { get; init; } + public required ResourceDto[] Resources { get; init; } - public required PublicId OrganizationId { get; init; } + public required ResourceGroupDto[] ResourceGroups { get; init; } - public required string Name { get; init; } + public required ResourcePlannerViewDto[] Views { get; init; } } diff --git a/src/Turnierplan.App/Models/ResourcePlannerHeaderDto.cs b/src/Turnierplan.App/Models/ResourcePlannerHeaderDto.cs new file mode 100644 index 00000000..fb95a335 --- /dev/null +++ b/src/Turnierplan.App/Models/ResourcePlannerHeaderDto.cs @@ -0,0 +1,12 @@ +using Turnierplan.Core.PublicId; + +namespace Turnierplan.App.Models; + +public sealed record ResourcePlannerHeaderDto +{ + public required PublicId Id { get; init; } + + public required PublicId OrganizationId { get; init; } + + public required string Name { get; init; } +} \ No newline at end of file diff --git a/src/Turnierplan.App/Models/ResourcePlannerViewDto.cs b/src/Turnierplan.App/Models/ResourcePlannerViewDto.cs new file mode 100644 index 00000000..2d942620 --- /dev/null +++ b/src/Turnierplan.App/Models/ResourcePlannerViewDto.cs @@ -0,0 +1,16 @@ +using Turnierplan.Core.PublicId; + +namespace Turnierplan.App.Models; + +public sealed record ResourcePlannerViewDto +{ + public required long Id { get; init; } + + public required PublicId PublicId { get; init; } + + public required bool IsActive { get; init; } + + public required bool DisplayAllGroups { get; init; } + + public required long[] ResourceGroupIds { get; init; } +} \ No newline at end of file diff --git a/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs index e3270c83..2bd5d3f3 100644 --- a/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs +++ b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs @@ -1,11 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using Turnierplan.Core.PublicId; using Turnierplan.Core.ResourcePlanner; namespace Turnierplan.Dal.Repositories; public interface IResourcePlannerRepository : IRepositoryWithPublicId { + Task GetByPublicIdAsync(PublicId id, Includes includes); + + [Flags] + public enum Includes + { + None = 0, + ResourceGroupsWithResources = 1, + ResourcePlannerViews = 2, + + All = ResourceGroupsWithResources | ResourcePlannerViews + } } internal sealed class ResourcePlannerRepository(TurnierplanContext context) : RepositoryBaseWithPublicId(context), IResourcePlannerRepository { + public async Task GetByPublicIdAsync(PublicId id, IResourcePlannerRepository.Includes includes) + { + var query = DbSet.Where(x => x.PublicId == id); + + if (includes.HasFlag(IResourcePlannerRepository.Includes.ResourceGroupsWithResources)) + { + query = query.Include(x => x.ResourceGroups).ThenInclude(x => x.ResourceAssignments).ThenInclude(x => x.Resource); + } + + if (includes.HasFlag(IResourcePlannerRepository.Includes.ResourcePlannerViews)) + { + query = query.Include(x => x.ResourcePlannerViews).ThenInclude(x => x.ResourceGroups); + } + + query = query.Include(x => x.Organization).ThenInclude(x => x.RoleAssignments); + query = query.Include(x => x.RoleAssignments); + + query = query.AsSplitQuery(); + + return await query.FirstOrDefaultAsync(); + } } From 6cee39447b2a85f216aded6326b32234eb4cee8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 11:16:05 +0200 Subject: [PATCH 14/18] Initial navigation on view resource planner page --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 7 ++ .../view-resource-planner.component.html | 40 +++++++++- .../view-resource-planner.component.ts | 79 ++++++++++++++++++- .../Client/src/app/portal/portal.routes.ts | 3 +- 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index d293a036..4d65565d 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -1004,6 +1004,13 @@ export const de = { OrganizationNotice: 'Es wird eine neue Helferliste in der Organisation {{organizationName}} angelegt.', Submit: 'Erstellen' }, + ViewResourcePlanner: { + Pages: { + Resources: 'Ressourcen', + Views: 'Views', + Settings: 'Einstellungen' + } + }, ViewTournamentPlanner: { Pages: { TournamentClasses: 'Turnierklassen', diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html index 71d2f849..e8cceabc 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html @@ -1 +1,39 @@ -

view-resource-planner works!

+ + @if (resourcePlanner) { + + + + + + @if (currentPage !== 0) { +
+ @switch (currentPage) { + @case (1) { + + views + } + @case (2) { + + settings + } + } +
+ } +
+ + + @if (currentPage === 0) { + + resources + } + +
+ } +
diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts index 861f3cc8..bdf0ee82 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts @@ -1,7 +1,82 @@ import { Component } from '@angular/core'; +import { PageFrameComponent, PageFrameNavigationTab } from '../../components/page-frame/page-frame.component'; +import { Actions } from '../../../generated/actions'; +import { ResourcePlannerDto } from '../../../api/models/resource-planner-dto'; +import { LoadingState, LoadingStateDirective } from '../../directives/loading-state.directive'; +import { of, Subject, switchMap, takeUntil } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { TurnierplanApi } from '../../../api/turnierplan-api'; +import { TitleService } from '../../services/title.service'; +import { getResourcePlanner } from '../../../api/fn/resource-planners/get-resource-planner'; @Component({ - imports: [], + imports: [PageFrameComponent, LoadingStateDirective], templateUrl: './view-resource-planner.component.html' }) -export class ViewResourcePlannerComponent {} +export class ViewResourcePlannerComponent { + protected loadingState: LoadingState = { isLoading: true }; + protected resourcePlanner?: ResourcePlannerDto; + + protected currentPage = 0; + protected pages: PageFrameNavigationTab[] = [ + { + id: 0, + title: 'Portal.ViewResourcePlanner.Pages.Resources', + icon: 'bi-x-diamond' + }, + { + id: 1, + title: 'Portal.ViewResourcePlanner.Pages.Views', + icon: 'bi-tags' + }, + { + id: 2, + title: 'Portal.ViewResourcePlanner.Pages.Settings', + icon: 'bi-gear', + authorization: Actions.GenericWrite + } + ]; + + private readonly destroyed$ = new Subject(); + + constructor( + private readonly turnierplanApi: TurnierplanApi, + private readonly route: ActivatedRoute, + private readonly titleService: TitleService + ) {} + + public ngOnInit(): void { + this.route.paramMap + .pipe( + takeUntil(this.destroyed$), + switchMap((params) => { + const resourcePlannerId = params.get('id'); + if (resourcePlannerId === null) { + this.loadingState = { isLoading: false }; + return of(); + } + this.loadingState = { isLoading: true }; + return this.turnierplanApi.invoke(getResourcePlanner, { id: resourcePlannerId }); + }) + ) + .subscribe({ + next: (resourcePlanner) => { + this.resourcePlanner = resourcePlanner; + this.titleService.setTitleFrom(resourcePlanner); + this.loadingState = { isLoading: false }; + }, + error: (error) => { + this.loadingState = { isLoading: false, error: error }; + } + }); + } + + public ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + protected togglePage(number: number): void { + this.currentPage = number; + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts b/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts index 834a4d86..66b66fe0 100644 --- a/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts +++ b/src/Turnierplan.App/Client/src/app/portal/portal.routes.ts @@ -87,8 +87,7 @@ export const portalRoutes: Routes = [ }, { path: 'resource-planner/:id', - component: ViewResourcePlannerComponent, - canDeactivate: [discardChangesGuard] + component: ViewResourcePlannerComponent }, { path: 'tournament-planner/:id', From 8266cd6f2cbd03aa81c687726455643ae537f1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 11:38:55 +0200 Subject: [PATCH 15/18] Some fixes for rbac management --- .../rbac-offcanvas/rbac-offcanvas.component.ts | 3 +++ .../RoleAssignments/CreateRoleAssignmentEndpoint.cs | 2 ++ .../RoleAssignments/DeleteRoleAssignmentEndpoint.cs | 2 ++ .../RoleAssignments/GetRoleAssignmentsEndpoint.cs | 2 ++ .../Repositories/ResourcePlannerRepository.cs | 9 +++++++++ 5 files changed, 18 insertions(+) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts index 5e7aaceb..71ad7c36 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -79,6 +79,9 @@ export class RbacOffcanvasComponent implements OnDestroy { case 'Organization': this.targetIcon = 'boxes'; break; + case 'ResourcePlanner': + this.targetIcon = 'file-earmark-spreadsheet'; + break; case 'Tournament': this.targetIcon = 'trophy'; break; diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs index 3da34ccb..f4bf42b7 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs @@ -12,6 +12,7 @@ using Turnierplan.Core.Image; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; @@ -51,6 +52,7 @@ private static async Task Handle( "Folder" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), "Image" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), "Organization" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "ResourcePlanner" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), "Tournament" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), "TournamentPlanner" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), "Venue" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs index 7f672125..1b9f8c36 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs @@ -7,6 +7,7 @@ using Turnierplan.Core.Image; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; @@ -46,6 +47,7 @@ private static async Task Handle( "Folder" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), "Image" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), "Organization" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "ResourcePlanner" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), "Tournament" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), "TournamentPlanner" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), "Venue" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs index 5551825a..27ad3fe0 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs @@ -9,6 +9,7 @@ using Turnierplan.Core.Image; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; +using Turnierplan.Core.ResourcePlanner; using Turnierplan.Core.Tournament; using Turnierplan.Core.TournamentPlanner; using Turnierplan.Core.Venue; @@ -41,6 +42,7 @@ private static async Task Handle( "Folder" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), "Image" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), "Organization" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "ResourcePlanner" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), "Tournament" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), "TournamentPlanner" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), "Venue" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), diff --git a/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs index 2bd5d3f3..88aae873 100644 --- a/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs +++ b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs @@ -21,6 +21,15 @@ public enum Includes internal sealed class ResourcePlannerRepository(TurnierplanContext context) : RepositoryBaseWithPublicId(context), IResourcePlannerRepository { + public override Task GetByPublicIdAsync(PublicId id) + { + return DbSet.Where(x => x.PublicId == id) + .Include(x => x.Organization).ThenInclude(x => x.RoleAssignments) + .Include(x => x.RoleAssignments) + .AsSplitQuery() + .FirstOrDefaultAsync(); + } + public async Task GetByPublicIdAsync(PublicId id, IResourcePlannerRepository.Includes includes) { var query = DbSet.Where(x => x.PublicId == id); From d66034a6d630c5e534a337c8086484f20ad31d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 11:40:35 +0200 Subject: [PATCH 16/18] Add settings page --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 20 ++++++++++- .../view-resource-planner.component.html | 34 +++++++++++++++++-- .../view-resource-planner.component.ts | 27 +++++++++++++-- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index 4d65565d..7cdc32f3 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -1007,8 +1007,26 @@ export const de = { ViewResourcePlanner: { Pages: { Resources: 'Ressourcen', - Views: 'Views', + Views: 'Ansichten', Settings: 'Einstellungen' + }, + Settings: { + Rename: { + Button: 'Umbenennen', + Title: 'Helferliste umbenennen', + EnterNewName: 'Geben Sie den neuen Namen für die Helferliste ein:' + } + }, + RbacWidget: { + Info: 'Verwalten Sie, welche Nutzer auf diese Helferliste zugreifen können und welche Aktionen sie durchführen können.' + }, + DeleteWidget: { + Title: 'Helferliste löschen', + Info: 'Wenn Sie eine Helferliste löschen, werden alle gespeicherten Ressourcen ebenfalls gelöscht.', + SuccessToast: { + Title: 'Helferliste wurde gelöscht', + Message: 'Ihre Helferliste wurde gelöscht.' + } } }, ViewTournamentPlanner: { diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html index e8cceabc..e2c665cd 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html @@ -8,7 +8,23 @@ [contextEntityId]="resourcePlanner.id" [rememberNavigationTab]="true" (navigationTabSelected)="togglePage($event.id)"> - + + @switch (currentPage) { + @case (0) { + + } + @case (1) { + + } + @case (2) { + + + } + } + @@ -21,7 +37,21 @@ } @case (2) { - settings + +
+ +
+
+
+
+ +
} } diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts index bdf0ee82..37a57b25 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts @@ -8,12 +8,25 @@ import { ActivatedRoute } from '@angular/router'; import { TurnierplanApi } from '../../../api/turnierplan-api'; import { TitleService } from '../../services/title.service'; import { getResourcePlanner } from '../../../api/fn/resource-planners/get-resource-planner'; +import { RenameButtonComponent } from '../../components/rename-button/rename-button.component'; +import { DeleteWidgetComponent } from '../../components/delete-widget/delete-widget.component'; +import { IsActionAllowedDirective } from '../../directives/is-action-allowed.directive'; +import { RbacWidgetComponent } from '../../components/rbac-widget/rbac-widget.component'; @Component({ - imports: [PageFrameComponent, LoadingStateDirective], + imports: [ + PageFrameComponent, + LoadingStateDirective, + RenameButtonComponent, + DeleteWidgetComponent, + IsActionAllowedDirective, + RbacWidgetComponent + ], templateUrl: './view-resource-planner.component.html' }) export class ViewResourcePlannerComponent { + protected readonly Actions = Actions; + protected loadingState: LoadingState = { isLoading: true }; protected resourcePlanner?: ResourcePlannerDto; @@ -22,12 +35,12 @@ export class ViewResourcePlannerComponent { { id: 0, title: 'Portal.ViewResourcePlanner.Pages.Resources', - icon: 'bi-x-diamond' + icon: 'bi-box-seam' }, { id: 1, title: 'Portal.ViewResourcePlanner.Pages.Views', - icon: 'bi-tags' + icon: 'bi-display' }, { id: 2, @@ -79,4 +92,12 @@ export class ViewResourcePlannerComponent { protected togglePage(number: number): void { this.currentPage = number; } + + protected renameResourcePlanner(name: string): void { + alert('Rename not implemented yet'); // TODO: Implement + } + + protected deleteResourcePlanner(): void { + alert('Delete not implemented yet'); // TODO: Implement + } } From 21463e7c19bc43a62607b29b5a2b6f5d2579a803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Jul 2026 12:43:51 +0200 Subject: [PATCH 17/18] Update to-do comments --- .../view-resource-planner/view-resource-planner.component.ts | 4 ++-- src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs | 2 +- .../ResourcePlanner/ResourceAssignmentState.cs | 2 +- src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs | 4 ++-- src/Turnierplan.Core/ResourcePlanner/ResourceType.cs | 3 +-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts index 37a57b25..4efae35b 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts @@ -94,10 +94,10 @@ export class ViewResourcePlannerComponent { } protected renameResourcePlanner(name: string): void { - alert('Rename not implemented yet'); // TODO: Implement + alert('Rename not implemented yet'); // TODO: Implement resource planner rename } protected deleteResourcePlanner(): void { - alert('Delete not implemented yet'); // TODO: Implement + alert('Delete not implemented yet'); // TODO: Implement delete resource planner } } diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs index bc5f8e66..4b31bb13 100644 --- a/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignment.cs @@ -11,7 +11,7 @@ internal ResourceAssignment(ResourceGroup resourceGroup, Resource resource) { ResourceGroup = resourceGroup; Resource = resource; - State = ResourceAssignmentState.Confirmed; // Todo: What's the default state for a newly assigned resource? + State = ResourceAssignmentState.Confirmed; // TODO: Determine what the default state for a newly assigned resource should be } public ResourceGroup ResourceGroup { get; internal set; } = null!; diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs index c05bec9f..819e132b 100644 --- a/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceAssignmentState.cs @@ -5,5 +5,5 @@ public enum ResourceAssignmentState Proposed = 1, Requested = 2, Confirmed = 3, - // ...Todo: Do we need these states, additional ones, other ones? + // TODO: Do we need these states, additional ones, other ones? } diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs index 0b43fbd5..103a3349 100644 --- a/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs @@ -4,6 +4,6 @@ public enum ResourceGroupType { // Note: Don't change enum values (DB serialization) - Workshift = 1, // Todo: Name? - General = 2 // Todo: Name? + Workshift = 1, + General = 2 } diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs index 83b2ada5..98b6b667 100644 --- a/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs @@ -5,6 +5,5 @@ public enum ResourceType // Note: Don't change enum values (DB serialization) Personnel = 1, - Commodity = 2, - // ...Todo: Do we need other resource types? + Commodity = 2 } From 386409bc818f8b77e0c6573d3d004fdd014b6002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Tue, 28 Jul 2026 10:18:15 +0200 Subject: [PATCH 18/18] Simplify if statements and fix error in condition --- .../ResourcePlanner/ResourceGroup.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs index 6c89f945..4a5faf19 100644 --- a/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs @@ -31,20 +31,14 @@ internal ResourceGroup(ResourcePlanner resourcePlanner, string? name, string? de throw new TurnierplanException($"Invalid resource type: '{type}'"); } - if (isWorkshift) + if (isWorkshift && (!start.HasValue || !end.HasValue)) { - if (!start.HasValue || end.HasValue) - { - throw new TurnierplanException($"Start and end time must be set if the type is '{ResourceGroupType.Workshift}'"); - } + throw new TurnierplanException($"Start and end time must be set if the type is '{ResourceGroupType.Workshift}'"); } - if (isGeneral) + if (isGeneral && string.IsNullOrWhiteSpace(name)) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new TurnierplanException($"Name must be a non-empty string if the type is '{ResourceGroupType.General}'"); - } + throw new TurnierplanException($"Name must be a non-empty string if the type is '{ResourceGroupType.General}'"); } Id = 0;