@switch (currentPage) {
@@ -61,7 +65,15 @@
@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 4efae35b..bbfa0f40 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
@@ -4,7 +4,7 @@ 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 { ActivatedRoute, Router } 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';
@@ -12,6 +12,11 @@ import { RenameButtonComponent } from '../../components/rename-button/rename-but
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';
+import { setResourcePlannerName } from '../../../api/fn/resource-planners/set-resource-planner-name';
+import { SmallSpinnerComponent } from '../../../core/components/small-spinner/small-spinner.component';
+import { NotificationService } from '../../../core/services/notification.service';
+import { deleteResourcePlanner } from '../../../api/fn/resource-planners/delete-resource-planner';
+import { ManageResourcesComponent } from '../../components/manage-resources/manage-resources.component';
@Component({
imports: [
@@ -20,7 +25,9 @@ import { RbacWidgetComponent } from '../../components/rbac-widget/rbac-widget.co
RenameButtonComponent,
DeleteWidgetComponent,
IsActionAllowedDirective,
- RbacWidgetComponent
+ RbacWidgetComponent,
+ SmallSpinnerComponent,
+ ManageResourcesComponent
],
templateUrl: './view-resource-planner.component.html'
})
@@ -29,13 +36,14 @@ export class ViewResourcePlannerComponent {
protected loadingState: LoadingState = { isLoading: true };
protected resourcePlanner?: ResourcePlannerDto;
+ protected isUpdatingName = false;
protected currentPage = 0;
protected pages: PageFrameNavigationTab[] = [
{
id: 0,
title: 'Portal.ViewResourcePlanner.Pages.Resources',
- icon: 'bi-box-seam'
+ icon: 'bi-columns'
},
{
id: 1,
@@ -55,7 +63,9 @@ export class ViewResourcePlannerComponent {
constructor(
private readonly turnierplanApi: TurnierplanApi,
private readonly route: ActivatedRoute,
- private readonly titleService: TitleService
+ private readonly router: Router,
+ private readonly titleService: TitleService,
+ private readonly notificationService: NotificationService
) {}
public ngOnInit(): void {
@@ -94,10 +104,45 @@ export class ViewResourcePlannerComponent {
}
protected renameResourcePlanner(name: string): void {
- alert('Rename not implemented yet'); // TODO: Implement resource planner rename
+ if (!this.resourcePlanner || name === this.resourcePlanner.name || this.isUpdatingName) {
+ return;
+ }
+
+ this.isUpdatingName = true;
+
+ this.turnierplanApi.invoke(setResourcePlannerName, { id: this.resourcePlanner.id, body: { name: name } }).subscribe({
+ next: () => {
+ if (this.resourcePlanner) {
+ this.resourcePlanner.name = name;
+ this.titleService.setTitleFrom(this.resourcePlanner);
+ }
+ this.isUpdatingName = false;
+ },
+ error: (error) => {
+ this.loadingState = { isLoading: false, error: error };
+ }
+ });
}
protected deleteResourcePlanner(): void {
- alert('Delete not implemented yet'); // TODO: Implement delete resource planner
+ if (!this.resourcePlanner) {
+ return;
+ }
+
+ const organizationId = this.resourcePlanner.organizationId;
+ this.loadingState = { isLoading: true, error: undefined };
+ this.turnierplanApi.invoke(deleteResourcePlanner, { id: this.resourcePlanner.id }).subscribe({
+ next: () => {
+ this.notificationService.showNotification(
+ 'info',
+ 'Portal.ViewResourcePlanner.DeleteWidget.SuccessToast.Title',
+ 'Portal.ViewResourcePlanner.DeleteWidget.SuccessToast.Message'
+ );
+ void this.router.navigate([`../../organization/${organizationId}`], { relativeTo: this.route });
+ },
+ error: (error) => {
+ this.loadingState = { isLoading: false, error: error };
+ }
+ });
}
}
diff --git a/src/Turnierplan.App/Endpoints/ResourcePlanners/DeleteResourcePlannerEndpoint.cs b/src/Turnierplan.App/Endpoints/ResourcePlanners/DeleteResourcePlannerEndpoint.cs
new file mode 100644
index 00000000..e21e84a2
--- /dev/null
+++ b/src/Turnierplan.App/Endpoints/ResourcePlanners/DeleteResourcePlannerEndpoint.cs
@@ -0,0 +1,40 @@
+using Microsoft.AspNetCore.Mvc;
+using Turnierplan.App.Security;
+using Turnierplan.Core.PublicId;
+using Turnierplan.Dal.Repositories;
+
+namespace Turnierplan.App.Endpoints.ResourcePlanners;
+
+internal sealed class DeleteResourcePlannerEndpoint : EndpointBase
+{
+ protected override HttpMethod Method => HttpMethod.Delete;
+
+ 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,
+ CancellationToken cancellationToken)
+ {
+ var resourcePlanner = await repository.GetByPublicIdAsync(id);
+
+ if (resourcePlanner is null)
+ {
+ return Results.NotFound();
+ }
+
+ if (!accessValidator.IsActionAllowed(resourcePlanner, Actions.GenericWrite))
+ {
+ return Results.Forbid();
+ }
+
+ repository.Remove(resourcePlanner);
+
+ await repository.UnitOfWork.SaveChangesAsync(cancellationToken);
+
+ return Results.NoContent();
+ }
+}
diff --git a/src/Turnierplan.App/Endpoints/ResourcePlanners/SetResourcePlannerNameEndpoint.cs b/src/Turnierplan.App/Endpoints/ResourcePlanners/SetResourcePlannerNameEndpoint.cs
new file mode 100644
index 00000000..cec8f1dc
--- /dev/null
+++ b/src/Turnierplan.App/Endpoints/ResourcePlanners/SetResourcePlannerNameEndpoint.cs
@@ -0,0 +1,64 @@
+using FluentValidation;
+using Microsoft.AspNetCore.Mvc;
+using Turnierplan.App.Extensions;
+using Turnierplan.App.Security;
+using Turnierplan.Core.PublicId;
+using Turnierplan.Dal.Repositories;
+
+namespace Turnierplan.App.Endpoints.ResourcePlanners;
+
+internal sealed class SetResourcePlannerNameEndpoint : EndpointBase
+{
+ protected override HttpMethod Method => HttpMethod.Patch;
+
+ protected override string Route => "/api/resource-planners/{id}/name";
+
+ protected override Delegate Handler => Handle;
+
+ private static async Task Handle(
+ [FromRoute] PublicId id,
+ [FromBody] SetResourcePlannerNameEndpointRequest request,
+ IResourcePlannerRepository repository,
+ IAccessValidator accessValidator,
+ CancellationToken cancellationToken)
+ {
+ if (!Validator.Instance.ValidateAndGetResult(request, out var result))
+ {
+ return result;
+ }
+
+ var resourcePlanner = await repository.GetByPublicIdAsync(id);
+
+ if (resourcePlanner is null)
+ {
+ return Results.NotFound();
+ }
+
+ if (!accessValidator.IsActionAllowed(resourcePlanner, Actions.GenericWrite))
+ {
+ return Results.Forbid();
+ }
+
+ resourcePlanner.Name = request.Name.Trim();
+
+ await repository.UnitOfWork.SaveChangesAsync(cancellationToken);
+
+ return Results.NoContent();
+ }
+
+ public sealed record SetResourcePlannerNameEndpointRequest
+ {
+ public required string Name { get; init; }
+ }
+
+ private sealed class Validator : AbstractValidator
+ {
+ public static readonly Validator Instance = new();
+
+ private Validator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty();
+ }
+ }
+}
diff --git a/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs
index cc9e1ffb..8bc5acfc 100644
--- a/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs
+++ b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs
@@ -34,8 +34,7 @@ protected override ResourcePlannerDto Map(IMapper mapper, MappingContext context
{
Id = group.Id,
Name = group.Name,
- Description = group.Description,
- Type = group.Type,
+ Notes = group.Notes,
Start = group.Start,
End = group.End,
Assignment =
diff --git a/src/Turnierplan.App/Models/ResourceGroupDto.cs b/src/Turnierplan.App/Models/ResourceGroupDto.cs
index 9d9ab26f..2a356ea1 100644
--- a/src/Turnierplan.App/Models/ResourceGroupDto.cs
+++ b/src/Turnierplan.App/Models/ResourceGroupDto.cs
@@ -6,11 +6,9 @@ public sealed record ResourceGroupDto
{
public required long Id { get; init; }
- public required string? Name { get; init; }
+ public required string Name { get; init; }
- public required string? Description { get; init; }
-
- public required ResourceGroupType Type { get; init; }
+ public required string? Notes { get; init; }
public required DateTime? Start { get; init; }
diff --git a/src/Turnierplan.Core/ResourcePlanner/Resource.cs b/src/Turnierplan.Core/ResourcePlanner/Resource.cs
index a2d32f6d..35d7419e 100644
--- a/src/Turnierplan.Core/ResourcePlanner/Resource.cs
+++ b/src/Turnierplan.Core/ResourcePlanner/Resource.cs
@@ -16,7 +16,7 @@ internal Resource(long id, ResourceType type, string name, string? notes)
_notes = notes;
}
- public Resource(ResourcePlanner resourcePlanner, ResourceType type, string name, string? notes)
+ internal Resource(ResourcePlanner resourcePlanner, ResourceType type, string name, string? notes)
{
Id = 0;
ResourcePlanner = resourcePlanner;
@@ -34,7 +34,17 @@ public Resource(ResourcePlanner resourcePlanner, ResourceType type, string name,
public string Name
{
get => _name;
- set => _name = value.Trim();
+ set
+ {
+ var trimmed = value.Trim();
+
+ if (string.IsNullOrEmpty(trimmed))
+ {
+ throw new TurnierplanException($"The resource {nameof(Name)} must be a non-empty string.");
+ }
+
+ _name = trimmed;
+ }
}
public string? Notes
diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs
index 4a5faf19..0ebb6d6b 100644
--- a/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs
+++ b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs
@@ -7,132 +7,79 @@ public sealed class ResourceGroup : Entity
{
internal readonly List _resourceAssignments = [];
- private string? _name;
- private string? _description;
+ private string _name;
+ private string? _notes;
- internal ResourceGroup(long id, string? name, string? description, ResourceGroupType type, DateTime? start, DateTime? end)
+ internal ResourceGroup(long id, string name, string? notes, DateTime? start, DateTime? end)
{
Id = id;
- Type = type;
Start = start;
End = end;
_name = name;
- _description = description;
+ _notes = notes;
}
- internal ResourceGroup(ResourcePlanner resourcePlanner, string? name, string? description, ResourceGroupType type, DateTime? start, DateTime? end)
+ internal ResourceGroup(ResourcePlanner resourcePlanner, string name, string? notes, DateTime? start, DateTime? end)
{
- var isWorkshift = type is ResourceGroupType.Workshift;
- var isGeneral = type is ResourceGroupType.General;
-
- if (!isWorkshift && !isGeneral)
+ if (string.IsNullOrWhiteSpace(name))
{
- throw new TurnierplanException($"Invalid resource type: '{type}'");
+ throw new TurnierplanException($"{nameof(Name)} must be a non-empty string.");
}
- if (isWorkshift && (!start.HasValue || !end.HasValue))
+ if (notes is not null && notes.Trim().Length == 0)
{
- throw new TurnierplanException($"Start and end time must be set if the type is '{ResourceGroupType.Workshift}'");
- }
-
- if (isGeneral && string.IsNullOrWhiteSpace(name))
- {
- throw new TurnierplanException($"Name must be a non-empty string if the type is '{ResourceGroupType.General}'");
+ throw new TurnierplanException($"{nameof(Notes)} must be null or a non-empty string.");
}
Id = 0;
ResourcePlanner = resourcePlanner;
- Type = type;
- Start = isWorkshift ? start : null;
- End = isWorkshift ? end : null;
+ Start = start;
+ End = end;
_name = name;
- _description = description;
+ _notes = notes;
}
public override long Id { get; protected set; }
public ResourcePlanner ResourcePlanner { get; internal set; } = null!;
- public string? Name
+ public string Name
{
get => _name;
set
{
- var trimmed = value?.Trim();
+ var trimmed = value.Trim();
- if (Type is ResourceGroupType.General)
+ if (string.IsNullOrEmpty(trimmed))
{
- 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");
+ throw new TurnierplanException($"{nameof(Name)} must be a non-empty string.");
}
_name = trimmed;
}
}
- public string? Description
+ public string? Notes
{
- get => _description;
+ get => _notes;
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}.");
+ throw new TurnierplanException($"{nameof(Notes)} must be null or a non-empty string");
}
- 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;
+ _notes = trimmed;
}
}
- 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}.");
- }
+ public DateTime? Start { get; set; }
- 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 DateTime? End { get; set; }
public IReadOnlyList ResourceAssignments => _resourceAssignments.AsReadOnly();
@@ -151,6 +98,15 @@ public void AssignResource(Resource resource)
_resourceAssignments.Add(new ResourceAssignment(this, resource));
}
+ public Resource AssignResource(ResourceType type, string name, string? notes)
+ {
+ var resource = new Resource(ResourcePlanner, type, name, notes);
+
+ _resourceAssignments.Add(new ResourceAssignment(this, resource));
+
+ return 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
deleted file mode 100644
index 103a3349..00000000
--- a/src/Turnierplan.Core/ResourcePlanner/ResourceGroupType.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Turnierplan.Core.ResourcePlanner;
-
-public enum ResourceGroupType
-{
- // Note: Don't change enum values (DB serialization)
-
- Workshift = 1,
- General = 2
-}
diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs
index 7001a54e..0687b2f9 100644
--- a/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs
+++ b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs
@@ -42,7 +42,7 @@ internal ResourcePlanner(long id, PublicId.PublicId publicId, DateTime createdAt
public DateTime CreatedAt { get; }
- public string Name { get; }
+ public string Name { get; set; }
public RoleAssignment AddRoleAssignment(Role role, Principal principal)
{
@@ -57,9 +57,9 @@ public void RemoveRoleAssignment(RoleAssignment roleAssignment)
_roleAssignments.Remove(roleAssignment);
}
- public ResourceGroup AddResourceGroup(string? name, string? description, ResourceGroupType type, DateTime? start, DateTime? end)
+ public ResourceGroup AddResourceGroup(string name, string? description, DateTime? start, DateTime? end)
{
- var resourceGroup = new ResourceGroup(this, name, description, type, start, end);
+ var resourceGroup = new ResourceGroup(this, name, description, start, end);
_resourceGroups.Add(resourceGroup);
return resourceGroup;
diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs
index 08a47662..c539fea4 100644
--- a/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs
+++ b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs
@@ -15,13 +15,11 @@ public void Configure(EntityTypeBuilder builder)
builder.Property(x => x.Id)
.IsRequired();
- builder.Property(x => x.Name);
-
- builder.Property(x => x.Description);
-
- builder.Property(x => x.Type)
+ builder.Property(x => x.Name)
.IsRequired();
+ builder.Property(x => x.Notes);
+
builder.Property(x => x.Start);
builder.Property(x => x.End);
diff --git a/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs b/src/Turnierplan.Dal/Migrations/20260729083005_Add_ResourcePlanner.Designer.cs
similarity index 99%
rename from src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs
rename to src/Turnierplan.Dal/Migrations/20260729083005_Add_ResourcePlanner.Designer.cs
index bbf5e99f..d6cff0a3 100644
--- a/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.Designer.cs
+++ b/src/Turnierplan.Dal/Migrations/20260729083005_Add_ResourcePlanner.Designer.cs
@@ -13,7 +13,7 @@
namespace Turnierplan.Dal.Migrations
{
[DbContext(typeof(TurnierplanContext))]
- [Migration("20260717182006_Add_ResourcePlanner")]
+ [Migration("20260729083005_Add_ResourcePlanner")]
partial class Add_ResourcePlanner
{
///
@@ -342,13 +342,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
- b.Property("Description")
- .HasColumnType("text");
-
b.Property("End")
.HasColumnType("timestamp with time zone");
b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
.HasColumnType("text");
b.Property("ResourcePlannerId")
@@ -357,9 +358,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("Start")
.HasColumnType("timestamp with time zone");
- b.Property("Type")
- .HasColumnType("integer");
-
b.HasKey("Id");
b.HasIndex("ResourcePlannerId");
diff --git a/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs b/src/Turnierplan.Dal/Migrations/20260729083005_Add_ResourcePlanner.cs
similarity index 98%
rename from src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs
rename to src/Turnierplan.Dal/Migrations/20260729083005_Add_ResourcePlanner.cs
index 82ad7fc3..0d299505 100644
--- a/src/Turnierplan.Dal/Migrations/20260717182006_Add_ResourcePlanner.cs
+++ b/src/Turnierplan.Dal/Migrations/20260729083005_Add_ResourcePlanner.cs
@@ -67,9 +67,8 @@ protected override void Up(MigrationBuilder migrationBuilder)
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),
+ Name = table.Column(type: "text", nullable: false),
+ Notes = table.Column(type: "text", nullable: true),
Start = table.Column(type: "timestamp with time zone", nullable: true),
End = table.Column(type: "timestamp with time zone", nullable: true)
},
diff --git a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs
index 1b592ec7..f334aa7d 100644
--- a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs
+++ b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs
@@ -339,13 +339,14 @@ protected override void BuildModel(ModelBuilder modelBuilder)
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
- b.Property("Description")
- .HasColumnType("text");
-
b.Property("End")
.HasColumnType("timestamp with time zone");
b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
.HasColumnType("text");
b.Property("ResourcePlannerId")
@@ -354,9 +355,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Start")
.HasColumnType("timestamp with time zone");
- b.Property("Type")
- .HasColumnType("integer");
-
b.HasKey("Id");
b.HasIndex("ResourcePlannerId");