diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index a3f4b0fc..7cdc32f3 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,41 @@ 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' + }, + ViewResourcePlanner: { + Pages: { + Resources: 'Ressourcen', + 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: { Pages: { TournamentClasses: 'Turnierklassen', 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/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..e2c665cd --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.html @@ -0,0 +1,69 @@ + + @if (resourcePlanner) { + + + @switch (currentPage) { + @case (0) { + + } + @case (1) { + + } + @case (2) { + + + } + } + + + + + @if (currentPage !== 0) { +
+ @switch (currentPage) { + @case (1) { + + views + } + @case (2) { + + +
+ +
+
+
+
+ +
+ } + } +
+ } +
+ + + @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 new file mode 100644 index 00000000..4efae35b --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-resource-planner/view-resource-planner.component.ts @@ -0,0 +1,103 @@ +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'; +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, + 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; + + protected currentPage = 0; + protected pages: PageFrameNavigationTab[] = [ + { + id: 0, + title: 'Portal.ViewResourcePlanner.Pages.Resources', + icon: 'bi-box-seam' + }, + { + id: 1, + title: 'Portal.ViewResourcePlanner.Pages.Views', + icon: 'bi-display' + }, + { + 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; + } + + protected renameResourcePlanner(name: string): void { + alert('Rename not implemented yet'); // TODO: Implement resource planner rename + } + + protected deleteResourcePlanner(): void { + alert('Delete not implemented yet'); // TODO: Implement delete resource planner + } +} 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..66b66fe0 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,10 @@ export const portalRoutes: Routes = [ return `tournament-planner/${data.params['id']}`; } }, + { + path: 'resource-planner/:id', + component: ViewResourcePlannerComponent + }, { path: 'tournament-planner/:id', component: ViewTournamentPlannerComponent, 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/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/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/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.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/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..cc9e1ffb --- /dev/null +++ b/src/Turnierplan.App/Mapping/Rules/ResourcePlannerMappingRule.cs @@ -0,0 +1,67 @@ +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, + 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 new file mode 100644 index 00000000..5dff3fa0 --- /dev/null +++ b/src/Turnierplan.App/Models/ResourcePlannerDto.cs @@ -0,0 +1,20 @@ +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; } + + public required ResourceDto[] Resources { get; init; } + + public required ResourceGroupDto[] ResourceGroups { 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.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.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.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..4b31bb13 --- /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: Determine what the default state for a newly assigned resource should be + } + + 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..819e132b --- /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..4a5faf19 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceGroup.cs @@ -0,0 +1,158 @@ +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 && (!start.HasValue || !end.HasValue)) + { + 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}'"); + } + + 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..103a3349 --- /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, + General = 2 +} diff --git a/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs new file mode 100644 index 00000000..7001a54e --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourcePlanner.cs @@ -0,0 +1,85 @@ +using Turnierplan.Core.Entity; +using Turnierplan.Core.RoleAssignment; + +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) + { + 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 IReadOnlyList ResourceGroups => _resourceGroups.AsReadOnly(); + + public IReadOnlyList ResourcePlannerViews => _resourcePlannerViews.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); + } + + 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..98b6b667 --- /dev/null +++ b/src/Turnierplan.Core/ResourcePlanner/ResourceType.cs @@ -0,0 +1,9 @@ +namespace Turnierplan.Core.ResourcePlanner; + +public enum ResourceType +{ + // Note: Don't change enum values (DB serialization) + + Personnel = 1, + Commodity = 2 +} 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/ResourceAssignmentEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceAssignmentEntityTypeConfiguration.cs new file mode 100644 index 00000000..1110265a --- /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("ResourceGroupId", "ResourceId"); + + builder.Property(x => x.State) + .IsRequired(); + } +} diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceEntityTypeConfiguration.cs new file mode 100644 index 00000000..bf036ffe --- /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("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + } +} diff --git a/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ResourceGroupEntityTypeConfiguration.cs new file mode 100644 index 00000000..08a47662 --- /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("ResourceGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + builder.Metadata.FindNavigation(nameof(ResourceGroup.ResourceAssignments))!.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..9cecc856 --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerEntityTypeConfiguration.cs @@ -0,0 +1,53 @@ +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.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.HasMany(x => x.ResourceGroups) + .WithOne(x => x.ResourcePlanner) + .HasForeignKey("ResourcePlannerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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..b82ed9e7 --- /dev/null +++ b/src/Turnierplan.Dal/EntityConfigurations/ResourcePlannerViewEntityTypeConfiguration.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Turnierplan.Core.ResourcePlanner; +using Turnierplan.Dal.Converters; + +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.Property(x => x.PublicId) + .HasConversion(); + + builder.Property(x => x.IsActive) + .IsRequired(); + + builder.Property(x => x.DisplayAllGroups) + .IsRequired(); + + builder.HasMany(x => x.ResourceGroups) + .WithMany(); + } +} 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/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"); 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(); diff --git a/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs new file mode 100644 index 00000000..88aae873 --- /dev/null +++ b/src/Turnierplan.Dal/Repositories/ResourcePlannerRepository.cs @@ -0,0 +1,54 @@ +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 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); + + 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(); + } +} 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); diff --git a/src/Turnierplan.Dal/TurnierplanContext.cs b/src/Turnierplan.Dal/TurnierplanContext.cs index 749858e0..1a1d4471 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; @@ -66,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!; @@ -169,6 +182,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 +199,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());