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 @@

{{ translations.events_facebookLink() }}

-
- - - - -
+ + + +
@@ -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 }}) + } +
+ + +
+ } +
+
+ } +
+ } +
+
+ + + + + +
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",