Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export const de = {
Tournaments: 'Turniere',
Venues: 'Spielstätten',
TournamentPlanners: 'Turnierplaner',
ResourcePlanners: 'Helferlisten',
Images: 'Bilder',
ApiKeys: 'API-Schlüssel',
Settings: 'Einstellungen'
Expand All @@ -180,21 +181,26 @@ export const de = {
TournamentCount: 'Turniere',
VenueCount: 'Spielstätten',
TournamentPlannerCount: 'Turnierplaner',
ResourcePlannerCount: 'Helferlisten',
ImagesCount: 'Bilder',
ImagesTotalSize: 'Gesamtgröße',
ApiKeyCount: 'API-Schlüssel'
},
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.',
NoVenues:
'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: {
Expand Down Expand Up @@ -988,6 +994,41 @@ export const de = {
OrganizationNotice: 'Es wird ein neuer Turnierplaner in der Organisation <span class="fw-bold">{{organizationName}}</span> 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 <span class="fw-bold">{{organizationName}}</span> 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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<tp-page-frame
*tpLoadingState="loadingState"
[ultraSlim]="true"
[title]="'Portal.CreateResourcePlanner.Title' | translate"
[backLink]="'../..'">
<label for="create_resource_planner_name" class="form-label" translate="Portal.CreateResourcePlanner.Form.Name"></label>
<input
id="create_resource_planner_name"
type="text"
class="form-control"
[formControl]="resourcePlannerName"
[required]="true"
[minlength]="1"
[ngClass]="resourcePlannerName.dirty || resourcePlannerName.touched ? (resourcePlannerName.invalid ? 'is-invalid' : 'is-valid') : ''" />
<div class="invalid-feedback" translate="Portal.CreateResourcePlanner.Form.NameInvalid"></div>
<div class="valid-feedback" translate="Portal.CreateResourcePlanner.Form.NameValid"></div>

<hr />

<div class="form-text d-flex flex-row align-items-center">
<i class="bi bi-info-circle"></i>
<span
class="ms-2"
[innerHTML]="'Portal.CreateResourcePlanner.OrganizationNotice' | translate: { organizationName: organization?.name }"></span>
</div>

<hr />

<div class="d-flex flex-row justify-content-end">
<tp-action-button
[type]="'success'"
[title]="'Portal.CreateResourcePlanner.Submit'"
[disabled]="resourcePlannerName.invalid"
(buttonClick)="confirmButtonClicked()" />
</div>
</tp-page-frame>
Original file line number Diff line number Diff line change
@@ -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<void>();

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 };
}
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
[icon]="'upload'"
[routerLink]="'upload/image'" />
}
@case (6) {
<tp-action-button
*tpIsActionAllowed="[organization.id, Actions.GenericWrite]"
[title]="'Portal.ViewOrganization.NewResourcePlanner'"
[type]="'outline-success'"
[icon]="'plus-circle'"
[routerLink]="'create/resource-planner'" />
}
@case (2) {
<tp-action-button
*tpIsActionAllowed="[organization.id, Actions.GenericWrite]"
Expand Down Expand Up @@ -154,6 +162,38 @@
</div>
}
}
@case (6) {
@if (isLoadingResourcePlanners || !resourcePlanners) {
<tp-loading-indicator class="my-5" [marginY]="false" />
} @else {
<div class="card-header d-flex flex-row align-items-center">
<tp-badge context="ViewOrganization" label="ResourcePlannerCount" [value]="resourcePlanners.length" />
</div>
<div class="card-body">
@if (resourcePlanners.length === 0) {
<div class="d-flex flex-row justify-content-center py-4 text-center">
<span class="tp-text-pre-wrap" translate="Portal.ViewOrganization.NoResourcePlanners"></span>
</div>
} @else {
<div class="d-flex flex-column gap-3">
@for (resourcePlanner of resourcePlanners; track resourcePlanner.id) {
<div class="card shadow-sm">
<div class="card-body d-flex flex-column gap-2">
<div class="fs-5 fw-bold card-title mb-0">{{ resourcePlanner.name }}</div>
</div>
<div class="card-footer text-end pe-2">
<a class="card-link" [routerLink]="'/portal/resource-planner/' + resourcePlanner.id">
<span translate="Portal.ViewOrganization.OpenResourcePlanner"></span>
<i class="ms-2 bi bi-arrow-right-circle" aria-hidden="true"></i>
</a>
</div>
</div>
}
</div>
}
</div>
}
}
@case (2) {
<!-- If the number is modified, check the @if statement below -->
@if (isLoadingApiKeys || !apiKeys) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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',
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading