Skip to content

Commit

Permalink
feat: add registered player table to player-event view (#154)
Browse files Browse the repository at this point in the history
Closes: #19
  • Loading branch information
AnSch1510 authored Jul 28, 2024
1 parent b09273a commit b5ff39b
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"task.autoDetect": "off",
"[xml]": {
"editor.defaultFormatter": "DotJoshJohnson.xml"
}
},
"cSpell.words": ["Minigolf", "Timeslot", "Timeslots"]

// Remove comments if branch policies are used
// "git.branchProtection": ["main", "master"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createAction, on, props } from '@ngrx/store';
import { produce } from 'immer';

import { PlayerEventTimeslotRegistrationChanged } from '../../../models/realtime-events';
import { PlayerEventTimeslotRegistrationChangedRealtimeEvent } from '../../../models/realtime-events';
import { Reducers } from '../../utils';
import { EVENTS_ACTION_SCOPE } from '../consts';
import { eventEntityAdapter, EventsFeatureState } from '../events.state';

export const eventTimeslotRegistrationChangedAction = createAction(
`[${EVENTS_ACTION_SCOPE}] Event Timeslot Registration Changed`,
props<PlayerEventTimeslotRegistrationChanged>()
props<PlayerEventTimeslotRegistrationChangedRealtimeEvent>()
);

export const eventTimeslotRegistrationChangedReducers: Reducers<EventsFeatureState> = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createAction, on, props } from '@ngrx/store';
import { produce } from 'immer';

import { PlayerEventTimeslotRegistrationChangedRealtimeEvent } from '../../../models/realtime-events';
import { Reducers } from '../../utils';
import { PLAYER_EVENTS_ACTION_SCOPE } from '../consts';
import { playerEventEntityAdapter, PlayerEventsFeatureState } from '../player-events.state';

export const eventTimeslotRegistrationChangedAction = createAction(
`[${PLAYER_EVENTS_ACTION_SCOPE}] Event Timeslot Registration Changed`,
props<PlayerEventTimeslotRegistrationChangedRealtimeEvent>()
);

export const eventTimeslotRegistrationChangedReducers: Reducers<PlayerEventsFeatureState> = [
on(eventTimeslotRegistrationChangedAction, (state, event) =>
playerEventEntityAdapter.mapOne(
{
id: event.eventId,
map: produce(draft => {
if (!draft.playerEventRegistrations) {
draft.playerEventRegistrations = [];
}
let registration = draft.playerEventRegistrations.find(x => x.userId == event.userId);
if (!registration) {
registration = {
userId: event.userId,
userAlias: event.userAlias,
registeredTimeslotIds: [],
};
draft.playerEventRegistrations.push(registration);
}
if (event.isRegistered) {
registration.registeredTimeslotIds.push(event.eventTimeslotId);
} else {
registration.registeredTimeslotIds = registration.registeredTimeslotIds.filter(
x => x !== event.eventTimeslotId
);
}
if (registration.registeredTimeslotIds.length === 0) {
draft.playerEventRegistrations = draft.playerEventRegistrations.filter(
x => x.userId !== event.userId
);
}
}),
},
state
)
),
];
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { inject } from '@angular/core';
import { EMPTY, merge, mergeMap, of } from 'rxjs';
import { EMPTY, map, merge, mergeMap, of } from 'rxjs';

import { eventTimeslotRegistrationChangedAction } from './actions/event-timeslot-registration-changed.action';
import { loadPlayerEventAction, loadPlayerEventEffects } from './actions/load-player-event.action';
import { loadPlayerEventsEffects } from './actions/load-player-events.action';
import { updateEventRegistrationEffects } from './actions/update-event-registration.action';
Expand Down Expand Up @@ -33,6 +34,12 @@ export const PlayerEventsFeatureEffects: Effects[] = [
)
),

eventTimeslotRegistrationChanged$: createFunctionalEffect.dispatching(() =>
inject(RealtimeEventsService).playerEventTimeslotRegistrationChanged.pipe(
map(event => eventTimeslotRegistrationChangedAction(event))
)
),

onServerReconnected$: createFunctionalEffect.dispatching(() =>
inject(RealtimeEventsService).onReconnected$.pipe(
mergeMap(() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createReducer, on } from '@ngrx/store';
import { produce } from 'immer';

import { eventTimeslotRegistrationChangedReducers } from './actions/event-timeslot-registration-changed.action';
import { loadPlayerEventReducers } from './actions/load-player-event.action';
import { loadPlayerEventsReducers } from './actions/load-player-events.action';
import { playerEventRemovedReducers } from './actions/player-event-removed.action';
Expand Down Expand Up @@ -28,6 +29,7 @@ export const playerEventsReducer = createReducer<PlayerEventsFeatureState>(
...playerEventRemovedReducers,
...resetPlayerEventsActionStateReducers,
...updateEventRegistrationReducers,
...eventTimeslotRegistrationChangedReducers,

on(addEventAction.success, (state, { response }) =>
state.actionStates.load.state === 'none'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@

@for (instance of x.instances; track $index) {
<div
class="flex flex-col gap-2 p-2"
[ngClass]="$index % 2 === 0 ? 'bg-surface-a' : 'bg-surface-b'"
class="flex flex-col gap-2 rounded p-2"
[ngClass]="$index % 2 === 0 ? 'bg-surface-c' : 'bg-surface-b'"
>
<div class="flex flex-row items-center gap-2 font-semibold">
<span class="i-[mdi--pound]"></span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
translations.events_timeslot_map()
}}</span>
</span>
<span class="truncate">{{ map.name }}</span>
<span>{{ map.name }}</span>
}
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,34 @@ <h1 class="m-0 text-center">{{ translations.playerEvents_notFound() }}</h1>
/>
</div>
}

<h2 class="m-0 pt-4">{{ translations.playerEvents_registeredPlayers() }}</h2>
<p-card>
<div
class="grid gap-2"
[style]="{ gridTemplateColumns: 'repeat(' + timeslots().length + ', auto) 1fr' }"
>
@for (timeslot of timeslots(); track timeslot.id) {
<div class="sticky top-0 text-xs">
{{ timeslot.time.hour | number: '2.0-0' }}:{{
timeslot.time.minute | number: '2.0-0'
}}
</div>
}
<div class="sticky top-0 text-xs">{{ translations.shared_name() }}</div>
@for (eventRegistration of eventRegistrations(); track eventRegistration.userId) {
@for (timeslot of timeslots(); track timeslot.id) {
<div
class="justify-self-center"
[class.i-[mdi--check]]="
eventRegistration.registeredTimeslotIds.includes(timeslot.id)
"
></div>
}
<div class="truncate">{{ eventRegistration.userAlias }}</div>
}
</div>
</p-card>
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export class PlayerEventDetailsComponent {
isActionBusy(this.registerActionState())
);
protected readonly event = selectSignal(computed(() => selectPlayerEvent(this.eventId())));
protected readonly eventRegistrations = computed(() =>
[...(this.event()?.playerEventRegistrations ?? [])].sort(
(a, b) => a.userAlias?.localeCompare(b.userAlias ?? '') ?? 1
)
);
protected readonly externalUri = computed(() => this.event()?.externalUri);
protected readonly timeslots = computed(() =>
[...(this.event()?.timeslots ?? [])].sort((a, b) => compareTimes(a.time, b.time))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,11 @@ export class UserDialogComponent {
}

protected submit() {
if (Object.values(this._allUsers()).some(x => x?.alias === this.form.value.alias)) {
if (
Object.values(this._allUsers()).some(
x => x?.id !== this.userToUpdate()?.id && x?.alias === this.form.value.alias
)
) {
this._messageService.add({
severity: 'error',
summary: this.translations.users_dialog_error_exists({
Expand Down
1 change: 1 addition & 0 deletions src/client/src/app/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
"backToEvents": "Zurück zu den Veranstaltungen",
"registrationClosed": "Die Anmeldung für diese Veranstaltung ist geschlossen.",
"registrations": "Anmeldungen",
"registeredPlayers": "Registrierte Spieler",
"playerAmount": "Spieleranzahl",
"fallbackTimeslot": "Ersatzzeitslot",
"fallbackTimeslotDescription": "Dieser Zeitslot ist nicht so beliebt wie die anderen. Wenn sich zu wenige Personen für diesen Zeitslot anmelden, kann es passieren, dass er nicht stattfindet. Daher kannst du einen Ersatzzeitslot angeben, der verwendet wird, wenn dieser Zeitslot nicht stattfindet.",
Expand Down
1 change: 1 addition & 0 deletions src/client/src/app/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
"backToEvents": "Back to events",
"registrationClosed": "The registration for this event is closed.",
"registrations": "Registrations",
"registeredPlayers": "Registered players",
"playerAmount": "Player count",
"fallbackTimeslot": "Fallback timeslot",
"fallbackTimeslotDescription": "This timeslot is not as popular as the others. It can happend that if too few peaple register for this timeslot, it will not take place. Therefore you can specify a fallback timeslot that will be used if this timeslot is canceled.",
Expand Down
3 changes: 2 additions & 1 deletion src/client/src/app/models/realtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export type PlayerEventRegistrationChangedRealtimeEvent = {
eventId: string;
};
export type UserSettingsChangedRealtimeEvent = {};
export type PlayerEventTimeslotRegistrationChanged = {
export type PlayerEventTimeslotRegistrationChangedRealtimeEvent = {
eventId: string;
eventTimeslotId: string;
userId: string;
userAlias: string | null | undefined;
isRegistered: boolean;
};
4 changes: 2 additions & 2 deletions src/client/src/app/services/realtime-events.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
PlayerEventChangedRealtimeEvent,
PlayerEventRegistrationChangedRealtimeEvent,
UserSettingsChangedRealtimeEvent,
PlayerEventTimeslotRegistrationChanged,
PlayerEventTimeslotRegistrationChangedRealtimeEvent,
EventInstancesEditorChangedEvent,
} from '../models/realtime-events';
import { SignalrRetryPolicy } from '../signalr-retry-policy';
Expand Down Expand Up @@ -69,7 +69,7 @@ export class RealtimeEventsService implements OnDestroy {
new EventEmitter<PlayerEventRegistrationChangedRealtimeEvent>();
public readonly userSettingsChanged = new EventEmitter<UserSettingsChangedRealtimeEvent>();
public readonly playerEventTimeslotRegistrationChanged =
new EventEmitter<PlayerEventTimeslotRegistrationChanged>();
new EventEmitter<PlayerEventTimeslotRegistrationChangedRealtimeEvent>();
public readonly isConnected = computed(() => !!this._isConnected());
public readonly onReconnected$ = toObservable(this._isConnected).pipe(
startWith(null),
Expand Down
13 changes: 13 additions & 0 deletions src/server/domain/Models/PlayerEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,22 @@ public record PlayerEvent(
[property: Required] DateTimeOffset RegistrationDeadline,
[property: Required] PlayerEventTimeslot[] Timeslots,
[property: Required] bool IsStarted,
PlayerEventRegistration[]? PlayerEventRegistrations,
string? ExternalUri
);

/// <summary>
/// Represents the timeslots for which a player has been registered.
/// </summary>
/// <param name="UserId">The id of the user.</param>
/// <param name="UserAlias">The alias of the user.</param>
/// <param name="RegisteredTimeslotIds">The ids of timeslots a player has been registered for.</param>
public record PlayerEventRegistration(
[property: Required] string UserId,
string? UserAlias,
[property: Required] string[] RegisteredTimeslotIds
);

/// <summary>
/// Represents a timeslot of an event that players can register to.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/server/domain/Models/RealtimeEvents/RealtimeEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public record PlayerEventTimeslotRegistrationChanged(
string EventId,
string EventTimeslotId,
string UserId,
string? UserAlias,
bool IsRegistered
) : IGroupRealtimeEvent
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,17 @@ await realtimeEventsService.SendEventAsync(
),
ct
);
var userAlias = await databaseContext
.Users.Where(x => x.Id == userId)
.Select(x => x.Alias)
.FirstOrDefaultAsync(ct);
var changeEvents = registrations
.Where(x => !targetRegistrations.Any(y => y.TimeslotId == x.EventTimeslotId))
.Select(x => new RealtimeEvent.PlayerEventTimeslotRegistrationChanged(
idService.Event.Encode(eventId),
idService.EventTimeslot.Encode(x.EventTimeslotId),
idService.User.Encode(userId),
userAlias,
false
))
.Concat(
Expand All @@ -173,6 +178,7 @@ await realtimeEventsService.SendEventAsync(
idService.Event.Encode(eventId),
idService.EventTimeslot.Encode(x.TimeslotId),
idService.User.Encode(userId),
userAlias,
true
))
);
Expand Down
13 changes: 13 additions & 0 deletions src/server/host/Mappers/PlayerEventMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public PlayerEvent Map(EventEntity entity, long userId)
.Select(timeslot => Map(timeslot, userId))
.ToArray(),
entity.StartedAt != null,
entity
.Timeslots.SelectMany(x => x.Registrations.Select(x => x.Player))
.DistinctBy(x => x.Id)
.Select(p => new PlayerEventRegistration(
idService.User.Encode(p.Id),
p.Alias,
entity
.Timeslots.Where(x => x.Registrations.Any(x => x.PlayerId == p.Id))
.Select(x => idService.EventTimeslot.Encode(x.Id))
.ToArray()
))
.ToArray(),
entity.ExternalUri
);
}
Expand Down Expand Up @@ -67,6 +79,7 @@ public IQueryable<EventEntity> AddIncludes(IQueryable<EventEntity> events)
return events
.Include(x => x.Timeslots)
.ThenInclude(x => x.Registrations)
.ThenInclude(x => x.Player)
.Include(x => x.Timeslots)
.ThenInclude(x => x.Instances)
.ThenInclude(x => x.Players)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public async Task GetPlayerEvent_Success(DatabaseProvider databaseProvider)
IsFallbackAllowed = x.IsFallbackAllowed,
Time = x.Time
})
.ToArray()
.ToArray(),
PlayerEventRegistrations = []
}
);
}
Expand Down Expand Up @@ -85,7 +86,20 @@ await user.CallApi(x =>
IsFallbackAllowed = x.IsFallbackAllowed,
Time = x.Time
})
.ToArray()
.ToArray(),
PlayerEventRegistrations =
[
new()
{
UserAlias = user.User.Alias,
RegisteredTimeslotIds =
[
@event.Timeslots.ElementAt(0).Id,
@event.Timeslots.ElementAt(2).Id
],
UserId = user.User.Id
}
]
};
expectedPlayerEvent
.Timeslots.Single(x => x.Id == @event.Timeslots.ElementAt(0).Id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ private static IEnumerable<PlayerEvent> ToPlayerEvents(IEnumerable<Event> events
IsFallbackAllowed = x.IsFallbackAllowed,
Time = x.Time
})
.ToArray()
.ToArray(),
PlayerEventRegistrations = []
});
}
}

0 comments on commit b5ff39b

Please sign in to comment.