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
12 changes: 11 additions & 1 deletion src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,12 +1055,22 @@ export const de = {
},
NoLabels: 'keine Labels',
NoLinkedTournament: 'kein Turnier verknüpft',
CannotDeleteTeamWhileLinked: 'Die Mannschaft kann nicht gelöscht werden, solange sie an einem Turnier teilnimmt.',
RenameTeam: {
Title: 'Mannschaft umbenennen',
EnterNewName:
'Geben Sie den neuen Mannschaftsnamen ein. Wenn diese Mannschaft einem Turnier hinzugefügt wurde, wird die Mannschaft im Turnier automatisch mit umbenannt.',
RequiredFeedback: 'Der Mannschaftsname darf nicht leer sein'
},
AddTeam: {
Button: 'Mannschaft hinzufügen',
Title: 'Mannschaft zur Anmeldung hinzufügen',
Confirm: 'Hinzufügen',
TournamentClass: 'Turnierklasse:',
TeamName: 'Mannschaftsname:',
TeamNameRequired: 'Der Mannschaftsname darf nicht leer sein'
},
NoTeams: 'Diese Anmeldung beinhaltet keine Mannschaften.',
HiddenTeamsTooltip: 'Diese Anmeldung beinhaltet weitere Mannschaften, welche nicht den Filterkriterien entsprechen',
HiddenTeams: '{{count}} weitere Mannschaft(en) werden aufgrund Ihrer Filterkriterien nicht angezeigt.',
HiddenTeamsShowAll: 'alle anzeigen',
Expand All @@ -1077,7 +1087,7 @@ export const de = {
CommentChanged: 'Bemerkung geändert',
TeamAdded: 'Mannschaft hinzugefügt',
TeamRenamed: 'Mannschaft umbenannt',
TeamRemoved: 'Mannschaft entfernt',
TeamRemoved: 'Mannschaft gelöscht',
LabelAdded: 'Label hinzugefügt',
LabelRemoved: 'Label entfernt'
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
<span class="text-decoration-underline">{{ teamName }}</span>
</div>
} @else if (changeLogType === ApplicationChangeLogType.TeamAdded) {
<!-- TODO: Implement with the following issue: https://github.com/turnierplan-NET/turnierplan.NET/issues/192 -->
<span class="value-display p-1 border text-success flex-grow-0">{{ teamName }}</span>
} @else if (changeLogType === ApplicationChangeLogType.TeamRemoved) {
<!-- TODO: Implement with the following issue: https://github.com/turnierplan-NET/turnierplan.NET/issues/192 -->
<span class="value-display p-1 border text-danger flex-grow-0">{{ teamName }}</span>
} @else {
<div class="d-flex flex-row align-items-start gap-2">
@if (previousValue !== undefined && previousValue.length > 0) {
Expand All @@ -38,7 +38,7 @@
<span class="mt-1 bi bi-arrow-right"></span>

@if (newValue !== undefined && newValue.length > 0) {
<span class="value-display p-1 border text-danger flex-grow-0">{{ newValue }}</span>
<span class="value-display p-1 border text-success flex-grow-0">{{ newValue }}</span>
} @else {
<span
class="value-display p-1 border text-secondary fst-italic flex-grow-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ export class ApplicationChangeLogEntryComponent {
break;
}
case ApplicationChangeLogType.TeamAdded: {
this.changeLogIcon = 'bi-plus-square';
this.changeLogIcon = 'bi-plus-circle';
this.teamName = propertyValue(ApplicationChangeLogProperty.TeamName);
break;
}
case ApplicationChangeLogType.TeamRemoved: {
this.changeLogIcon = 'bi-dash-square';
this.changeLogIcon = 'bi-trash';
this.teamName = propertyValue(ApplicationChangeLogProperty.TeamName);
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="modal-header">
<div class="modal-title" [translate]="'Portal.ViewPlanningRealm.Applications.AddTeam.Title'"></div>
<button type="button" class="btn-close" (click)="modal.dismiss()" [tabindex]="-1"></button>
</div>
<div class="modal-body d-flex flex-column">
<div class="form-group mb-2">
<label class="form-label" for="tournament_class" [translate]="'Portal.ViewPlanningRealm.Applications.AddTeam.TournamentClass'"></label>
<select class="form-select" [(ngModel)]="tournamentClassId">
@for (tournamentClass of tournamentClasses; track tournamentClass.id) {
<option [value]="tournamentClass.id">{{ tournamentClass.name }}</option>
}
</select>
</div>

<div class="form-group">
<label class="form-label" for="team_name" [translate]="'Portal.ViewPlanningRealm.Applications.AddTeam.TeamName'"></label>
<input class="form-control" type="text" id="team_name" [(ngModel)]="teamName" [class]="{ 'is-invalid': showError }" />

@if (showError) {
<div class="invalid-feedback" [translate]="'Portal.ViewPlanningRealm.Applications.AddTeam.TeamNameRequired'"></div>
}
</div>
</div>
<div class="modal-footer">
<tp-action-button [type]="'outline-dark'" [title]="'Portal.General.Cancel'" (buttonClick)="modal.dismiss()" />
<tp-action-button [type]="'success'" [title]="'Portal.ViewPlanningRealm.Applications.AddTeam.Confirm'" (buttonClick)="confirm()" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Component } from '@angular/core';
import { TranslateDirective } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { ActionButtonComponent } from '../action-button/action-button.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { PlanningRealmDto } from '../../../api/models/planning-realm-dto';
import { TournamentClassDto } from '../../../api/models/tournament-class-dto';
import { CreateApplicationTeamEndpointRequest } from '../../../api/models/create-application-team-endpoint-request';

@Component({
imports: [ActionButtonComponent, TranslateDirective, FormsModule],
templateUrl: './manage-applications-add-team.component.html'
})
export class ManageApplicationsAddTeamComponent {
protected tournamentClasses: TournamentClassDto[] = [];
protected teamName: string = '';
protected tournamentClassId: number = 0;
protected showError = false;

constructor(protected readonly modal: NgbActiveModal) {}

public init(planningRealm: PlanningRealmDto): void {
this.tournamentClasses = [...planningRealm.tournamentClasses].sort((a, b) => a.name.localeCompare(b.name));
this.tournamentClassId = this.tournamentClasses[0].id;
}

protected confirm(): void {
if (this.teamName.trim().length === 0) {
this.showError = true;
return;
}

const result: CreateApplicationTeamEndpointRequest = {
teamName: this.teamName.trim(),
tournamentClassId: this.tournamentClassId
};

this.modal.close(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,15 @@
</td>
<td class="text-nowrap align-correctly">{{ application.createdAt | translateDate: 'short' }}</td>
<td class="align-correctly">
<span>{{ visibleTeamsCount }}</span>
@if (visibleTeamsCount !== application.teams.length) {
<tp-tooltip-icon
[icon]="'exclamation-diamond'"
[tooltipText]="'Portal.ViewPlanningRealm.Applications.HiddenTeamsTooltip'" />
@if (application.teams.length === 0) {
<span class="fw-bold text-warning">0</span>
} @else {
<span>{{ visibleTeamsCount }}</span>
@if (visibleTeamsCount !== application.teams.length) {
<tp-tooltip-icon
[icon]="'exclamation-diamond'"
[tooltipText]="'Portal.ViewPlanningRealm.Applications.HiddenTeamsTooltip'" />
}
}
</td>
<td class="align-correctly">{{ application.contact }}</td>
Expand Down Expand Up @@ -137,13 +141,14 @@
@if (showApplicationTeams) {
<tr>
<td colspan="11">
<table style="max-width: 900px" class="mb-0 table table-sm table-bordered">
<table style="max-width: 900px" class="mb-2 table table-sm table-bordered">
<thead>
<tr>
<th translate="Portal.ViewPlanningRealm.Applications.TableHeader.TournamentClass"></th>
<th translate="Portal.ViewPlanningRealm.Applications.TableHeader.TeamName"></th>
<th translate="Portal.ViewPlanningRealm.Applications.TableHeader.Labels"></th>
<th translate="Portal.ViewPlanningRealm.Applications.TableHeader.Tournament"></th>
<th style="width: 1px"></th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -194,12 +199,28 @@
translate="Portal.ViewPlanningRealm.Applications.NoLinkedTournament"></span>
}
</td>
<td class="align-middle px-2">
@if (team.linkedTournament) {
<tp-tooltip-icon
[margin]="false"
[tooltipText]="'Portal.ViewPlanningRealm.Applications.CannotDeleteTeamWhileLinked'" />
} @else {
<tp-delete-button [reducedFootprint]="true" (confirmed)="deleteTeam(application.id, team.id)" />
}
</td>
</tr>
}
} @empty {
<tr>
<td colspan="5">
<span class="fst-italic small text-secondary" translate="Portal.ViewPlanningRealm.Applications.NoTeams"></span>
</td>
</tr>
}

@if (hiddenTeamsCount > 0) {
<tr>
<td colspan="4">
<td colspan="5">
<span
class="fst-italic small text-secondary"
translate="Portal.ViewPlanningRealm.Applications.HiddenTeams"
Expand All @@ -213,6 +234,12 @@
}
</tbody>
</table>

<tp-action-button
[icon]="'plus-circle'"
[type]="'outline-secondary'"
[title]="'Portal.ViewPlanningRealm.Applications.AddTeam.Button'"
(buttonClick)="addTeam(application.id)" />
</td>
</tr>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import { setApplicationTeamLabels } from '../../../api/fn/application-teams/set-
import { Actions } from '../../../generated/actions';
import { AuthorizationService } from '../../../core/services/authorization.service';
import { ApplicationChangeLogComponent } from '../application-change-log/application-change-log.component';
import { DeleteButtonComponent } from '../delete-button/delete-button.component';
import { deleteApplicationTeam } from '../../../api/fn/application-teams/delete-application-team';
import { ManageApplicationsAddTeamComponent } from '../manage-applications-add-team/manage-applications-add-team.component';
import { CreateApplicationTeamEndpointRequest } from '../../../api/models/create-application-team-endpoint-request';
import { createApplicationTeam } from '../../../api/fn/application-teams/create-application-team';

@Component({
selector: 'tp-manage-applications',
Expand All @@ -54,7 +59,8 @@ import { ApplicationChangeLogComponent } from '../application-change-log/applica
RenameButtonComponent,
NgStyle,
LabelComponent,
AsyncPipe
AsyncPipe,
DeleteButtonComponent
]
})
export class ManageApplicationsComponent implements OnDestroy {
Expand Down Expand Up @@ -268,6 +274,37 @@ export class ManageApplicationsComponent implements OnDestroy {
void this.router.navigate(['/portal/tournament/', tournamentId]);
}

protected addTeam(applicationId: number): void {
const ref = this.modalService.open(ManageApplicationsAddTeamComponent, {
centered: true,
size: 'md',
fullscreen: 'md'
});

const component = ref.componentInstance as ManageApplicationsAddTeamComponent;
component.init(this.planningRealm);

ref.closed
.pipe(
tap(() => (this.isLoading = true)),
switchMap((request: CreateApplicationTeamEndpointRequest) =>
this.turnierplanApi.invoke(createApplicationTeam, {
planningRealmId: this.planningRealm.id,
applicationId: applicationId,
body: request
})
)
)
.subscribe({
next: () => {
this.reload$.next(undefined); // reload will eventually set isLoading to false
},
error: (error) => {
this.errorOccured.emit(error);
}
});
}

protected renameTeam(applicationId: number, applicationTeam: ApplicationTeamDto, name: string): void {
this.turnierplanApi
.invoke(setApplicationTeamName, {
Expand Down Expand Up @@ -324,6 +361,25 @@ export class ManageApplicationsComponent implements OnDestroy {
});
}

protected deleteTeam(applicationId: number, applicationTeamId: number): void {
this.isLoading = true;

this.turnierplanApi
.invoke(deleteApplicationTeam, {
planningRealmId: this.planningRealm.id,
applicationId: applicationId,
applicationTeamId: applicationTeamId
})
.subscribe({
next: () => {
this.reload$.next(undefined); // reload will eventually set isLoading to false
},
error: (error) => {
this.errorOccured.emit(error);
}
});
}

protected setAllApplicationsExpanded(expanded: boolean): void {
this.expandedApplications = {};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Turnierplan.App.Extensions;
using Turnierplan.App.Security;
using Turnierplan.Core.PlanningRealm;
using Turnierplan.Core.PublicId;

namespace Turnierplan.App.Endpoints.ApplicationTeams;

internal sealed class CreateApplicationTeamEndpoint : EndpointBase
{
protected override HttpMethod Method => HttpMethod.Post;

protected override string Route => "/api/planning-realms/{planningRealmId}/applications/{applicationId:int}/teams";

protected override Delegate Handler => Handle;

private static async Task<IResult> Handle(
[FromRoute] PublicId planningRealmId,
[FromRoute] long applicationId,
[FromBody] CreateApplicationTeamEndpointRequest request,
IPlanningRealmRepository planningRealmRepository,
IAccessValidator accessValidator,
CancellationToken cancellationToken)
{
if (!Validator.Instance.ValidateAndGetResult(request, out var result))
{
return result;
}

var planningRealm = await planningRealmRepository.GetByPublicIdAsync(planningRealmId, IPlanningRealmRepository.Includes.TournamentClasses | IPlanningRealmRepository.Includes.Applications);

if (planningRealm is null)
{
return Results.NotFound();
}

if (!accessValidator.IsActionAllowed(planningRealm, Actions.ApplicationsWrite))
{
return Results.Forbid();
}

var application = planningRealm.Applications.FirstOrDefault(x => x.Id == applicationId);
var tournamentClass = planningRealm.TournamentClasses.FirstOrDefault(x => x.Id == request.TournamentClassId);

if (application is null || tournamentClass is null)
{
return Results.NotFound();
}

application.AddTeam(tournamentClass, request.TeamName);

await planningRealmRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

return Results.NoContent();
}

public sealed record CreateApplicationTeamEndpointRequest
{
public required long TournamentClassId { get; init; }

public required string TeamName { get; init; }
}

private sealed class Validator : AbstractValidator<CreateApplicationTeamEndpointRequest>
{
public static readonly Validator Instance = new();

private Validator()
{
RuleFor(x => x.TournamentClassId)
.GreaterThan(0);

RuleFor(x => x.TeamName)
.NotEmpty();
}
}
}
Loading
Loading