diff --git a/src/client/src/app/components/events/event-details/event-details.component.html b/src/client/src/app/components/events/event-details/event-details.component.html
index 84160be..a10e6ee 100644
--- a/src/client/src/app/components/events/event-details/event-details.component.html
+++ b/src/client/src/app/components/events/event-details/event-details.component.html
@@ -73,33 +73,31 @@
@@ -155,16 +153,29 @@
{{ translations.events_timeslots() }}
{{ translations.events_groups() }}
@if (!event.startedAt) {
-
+
+
+ @if (hasInstances()) {
+
+ }
+
}
@@ -234,3 +245,5 @@
{{ translations.events_notFound() }}
}
}
+
+
diff --git a/src/client/src/app/components/events/event-details/event-details.component.ts b/src/client/src/app/components/events/event-details/event-details.component.ts
index 75eee72..44f97df 100644
--- a/src/client/src/app/components/events/event-details/event-details.component.ts
+++ b/src/client/src/app/components/events/event-details/event-details.component.ts
@@ -36,6 +36,7 @@ import { ifTruthy } from '../../../utils/common.utils';
import { compareTimes } from '../../../utils/date.utils';
import { errorToastEffect, selectSignal } from '../../../utils/ngrx.utils';
import { EventFormComponent } from '../event-form/event-form.component';
+import { EventInstancesDialogComponent } from '../event-instances-dialog/event-instances-dialog.component';
import { EventTimeslotDialogComponent } from '../event-timeslot-dialog/event-timeslot-dialog.component';
@Component({
@@ -47,6 +48,7 @@ import { EventTimeslotDialogComponent } from '../event-timeslot-dialog/event-tim
CardModule,
CommonModule,
EventFormComponent,
+ EventInstancesDialogComponent,
EventTimeslotDialogComponent,
FormsModule,
InputGroupAddonModule,
diff --git a/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.html b/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.html
new file mode 100644
index 0000000..310e1e5
--- /dev/null
+++ b/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.html
@@ -0,0 +1,125 @@
+
+
+
+ @for (x of instances(); track x.timeslot.id) {
+
+
+
+
+ {{ x.timeslot.time.hour | number: '2.0-0' }}:{{
+ x.timeslot.time.minute | number: '2.0-0'
+ }}
+
+
+
+ @for (instance of x.instances; track $index) {
+
+
+
+
{{ instance.groupCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+ @for (playerId of instance.playerIds; track playerId; let pi = $index) {
+
+
+ @if (allUsers()[playerId]; as user) {
+ {{ user.alias }}
+ } @else {
+ <{{ translations.events_timeslot_unknownPlayer() }}>
+ ({{ playerId }})
+ }
+
+
+
+
+
+
+
+ @if (instance.id === '') {
+
+ {{ translations.shared_remove() }}
+ } @else {
+
+ {{ instance.groupCode }}
+ }
+
+
+
+
+
+
+ }
+
+
+ }
+
+ }
+
+
+
+
+
+
+
+
diff --git a/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts b/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts
new file mode 100644
index 0000000..c9f8c5c
--- /dev/null
+++ b/src/client/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts
@@ -0,0 +1,144 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
+import { produce } from 'immer';
+import { ButtonModule } from 'primeng/button';
+import { DialogModule } from 'primeng/dialog';
+import { InputGroupModule } from 'primeng/inputgroup';
+import { ListboxModule } from 'primeng/listbox';
+import { OverlayPanelModule } from 'primeng/overlaypanel';
+
+import { userSelectors } from '../../../+state/users';
+import { Event, EventInstance, EventTimeslot } from '../../../models/parsed-models';
+import { Logger } from '../../../services/logger.service';
+import { TranslateService } from '../../../services/translate.service';
+import { notNullish } from '../../../utils/common.utils';
+import { selectSignal } from '../../../utils/ngrx.utils';
+import { UserItemComponent } from '../../users/user-item/user-item.component';
+
+type EventInstances = { timeslot: EventTimeslot; instances: EventInstance[] }[];
+
+@Component({
+ selector: 'app-event-instances-dialog',
+ standalone: true,
+ imports: [
+ ButtonModule,
+ CommonModule,
+ DialogModule,
+ InputGroupModule,
+ ListboxModule,
+ OverlayPanelModule,
+ UserItemComponent,
+ ],
+ templateUrl: './event-instances-dialog.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class EventInstancesDialogComponent {
+ protected readonly translations = inject(TranslateService).translations;
+
+ private readonly _removeItem: EventInstance = {
+ id: '
',
+ groupCode: '',
+ playerIds: [],
+ };
+
+ protected readonly visible = signal(false);
+ protected readonly instances = signal([]);
+ protected readonly allUsers = selectSignal(userSelectors.selectEntities);
+ protected readonly unassignedUsers = computed(() =>
+ Object.fromEntries(
+ this.instances().map(({ timeslot, instances }) => {
+ const unassignedUsers = Object.values(this.allUsers())
+ .filter(notNullish)
+ .filter(u => !instances.some(i => i.playerIds.includes(u.id)));
+
+ const groups = [
+ {
+ label: this.translations.events_instancesDialog_registeredPlayers(),
+ items: unassignedUsers.filter(u => timeslot.playerIds.includes(u.id)),
+ },
+ {
+ label: this.translations.events_instancesDialog_unregisteredPlayers(),
+ items: unassignedUsers.filter(u => !timeslot.playerIds.includes(u.id)),
+ },
+ ];
+ return [timeslot.id, groups] as const;
+ })
+ )
+ );
+ protected readonly moveToInstanceOptions = computed(() =>
+ Object.fromEntries(
+ this.instances().map(
+ ({ timeslot, instances }) =>
+ [
+ timeslot.id,
+ Object.fromEntries(
+ instances.map(
+ instance =>
+ [
+ instance.id,
+ [
+ ...(this.instances()
+ .find(({ timeslot }) => timeslot.id === timeslot.id)
+ ?.instances.filter(i => i.id !== instance.id) ?? []),
+ this._removeItem,
+ ],
+ ] as [string, EventInstance[]]
+ )
+ ),
+ ] as const
+ )
+ )
+ );
+
+ protected readonly isBusy = signal(false);
+
+ public open(event: Event) {
+ this.instances.set(
+ event.timeslots.map(timeslot => ({ timeslot, instances: timeslot.instances }))
+ );
+ this.visible.set(true);
+ }
+
+ protected addPlayer(timeslotId: string, instanceId: string, playerId: string) {
+ Logger.logDebug('EventInstancesDialogComponent', 'addPlayer', {
+ timeslotId,
+ instanceId,
+ playerId,
+ });
+ this.instances.update(
+ produce(draft => {
+ const timeslot = draft.find(({ timeslot }) => timeslot.id === timeslotId);
+ const instance = timeslot?.instances.find(i => i.id === instanceId);
+ instance?.playerIds.push(playerId);
+ })
+ );
+ }
+
+ protected movePlayer(
+ timeslotId: string,
+ playerId: string,
+ oldInstanbceId: string,
+ newInstanceId: string
+ ) {
+ Logger.logDebug('EventInstancesDialogComponent', 'movePlayer', {
+ timeslotId,
+ playerId,
+ oldInstanbceId,
+ newInstanceId,
+ });
+ this.instances.update(
+ produce(draft => {
+ const timeslot = draft.find(({ timeslot }) => timeslot.id === timeslotId);
+ const oldInstance = timeslot?.instances.find(i => i.id === oldInstanbceId);
+ const newInstance = timeslot?.instances.find(i => i.id === newInstanceId);
+ oldInstance?.playerIds.splice(oldInstance.playerIds.indexOf(playerId), 1);
+ newInstance?.playerIds.push(playerId);
+ })
+ );
+ }
+
+ protected submit() {
+ Logger.logDebug('EventInstancesDialogComponent', 'submit', { instances: this.instances() });
+ // TODO: Implement Server-side logic and dispatch action
+ }
+}
diff --git a/src/client/src/app/i18n/de.json b/src/client/src/app/i18n/de.json
index 5eea8f6..383ae4b 100644
--- a/src/client/src/app/i18n/de.json
+++ b/src/client/src/app/i18n/de.json
@@ -80,6 +80,7 @@
"start": "Veranstaltung starten",
"groups": "Gruppen",
"buildGroups": "Gruppen bilden",
+ "editGroups": "Gruppen bearbeiten",
"rebuildGroups": "Gruppen neu bilden",
"commit": "Event freigeben",
"staged": "vorbereitet",
@@ -136,6 +137,11 @@
"title": "Veranstaltung freigeben",
"text": "Möchtest du die Veranstlaung {{date}} wirklich freigeben? Die Zeitfenster können nicht mehr angepasst werden und die Spieler werden über ein neues Event benachrichtigt"
},
+ "instancesDialog": {
+ "title": "Veranstaltungs-Instanzen",
+ "registeredPlayers": "Angemeldete Spieler",
+ "unregisteredPlayers": "Nicht angemeldete Spieler"
+ },
"warning": {
"notStartableNoInstances": "Die Veranstaltung kann nicht gestartet werden, da die Gruppen noch nicht gebildet wurden.",
"notStartableMissingMap": "Die Veranstaltung kann nicht gestartet werden, da nicht für alle gespielten Zeitslots eine Bahn festgelegt wurde."
@@ -257,6 +263,7 @@
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
+ "remove": "Entfernen",
"edit": "Bearbeiten",
"ok": "OK",
"email": "E-Mail",
diff --git a/src/client/src/app/i18n/en.json b/src/client/src/app/i18n/en.json
index 8127df2..cf58804 100644
--- a/src/client/src/app/i18n/en.json
+++ b/src/client/src/app/i18n/en.json
@@ -80,6 +80,7 @@
"start": "Start event",
"groups": "Groups",
"buildGroups": "Build groups",
+ "editGroups": "Edit groups",
"rebuildGroups": "Rebuild groups",
"commit": "commit event",
"staged": "staged",
@@ -136,6 +137,11 @@
"title": "Commit event",
"text": "Do you really want to release the event {{date}}? The time slots can no longer be adjusted and players will be notified of a new event"
},
+ "instancesDialog": {
+ "title": "Event instances",
+ "registeredPlayers": "Registered players",
+ "unregisteredPlayers": "Unregistered players"
+ },
"warning": {
"notStartableNoInstances": "The event cannot be started because the groups have not been built yet.",
"notStartableMissingMap": "The event cannot be started because not all played timeslots have a map assigned."
@@ -257,6 +263,7 @@
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
+ "remove": "Remove",
"edit": "Edit",
"ok": "OK",
"email": "Email",