Skip to content

Commit

Permalink
feat: add UI for editing event instances
Browse files Browse the repository at this point in the history
Refs: #24
  • Loading branch information
MaSch0212 committed Jul 21, 2024
1 parent 010c5de commit 8550f8e
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,31 @@

<div class="mt-4 flex flex-row items-center gap-2">
<h2 class="m-0 grow">{{ translations.events_facebookLink() }}</h2>
<div>
<p-inputGroup>
<button
type="button"
pButton
[label]="
externalUri() && externalUri() !== undefined && externalUri()?.length !== 0
? translations.shared_edit()
: translations.shared_add()
"
icon="i-[mdi--facebook]"
(click)="modifyExternalUriDialog.open(externalUri())"
></button>
<button
type="button"
pButton
class="bg-surface-a text-primary"
icon="i-[mdi--arrow-right-circle]"
[disabled]="
externalUri() && externalUri() !== undefined && externalUri()?.length !== 0
? false
: true
"
(click)="openExternalUri()"
></button>
</p-inputGroup>
</div>
<p-inputGroup class="w-auto">
<button
type="button"
pButton
[label]="
externalUri() && externalUri() !== undefined && externalUri()?.length !== 0
? translations.shared_edit()
: translations.shared_add()
"
icon="i-[mdi--facebook]"
(click)="modifyExternalUriDialog.open(externalUri())"
></button>
<button
type="button"
pButton
[outlined]="true"
icon="i-[mdi--arrow-right-circle]"
[disabled]="
externalUri() && externalUri() !== undefined && externalUri()?.length !== 0
? false
: true
"
(click)="openExternalUri()"
></button>
</p-inputGroup>
</div>

<div class="mt-4 flex flex-row items-center gap-2">
Expand Down Expand Up @@ -155,16 +153,29 @@ <h2 class="m-0 grow">{{ translations.events_timeslots() }}</h2>
<div class="flex flex-row items-center pt-4">
<h2 class="m-0 grow">{{ translations.events_groups() }}</h2>
@if (!event.startedAt) {
<p-button
icon="i-[mdi--shuffle]"
[label]="
hasInstances()
? translations.events_rebuildGroups()
: translations.events_buildGroups()
"
(onClick)="buildInstances()"
[loading]="isBuildBusy()"
/>
<p-inputGroup class="w-auto">
<button
pButton
icon="i-[mdi--shuffle]"
[label]="
hasInstances()
? translations.events_rebuildGroups()
: translations.events_buildGroups()
"
(click)="buildInstances()"
[loading]="isBuildBusy()"
></button>
@if (hasInstances()) {
<button
pButton
icon="i-[mdi--pencil]"
[outlined]="true"
[pTooltip]="translations.events_editGroups()"
(click)="instancesDialog.open(event)"
[loading]="isBuildBusy()"
></button>
}
</p-inputGroup>
}
</div>
<div class="flex shrink-0 flex-row overflow-auto text-xs">
Expand Down Expand Up @@ -234,3 +245,5 @@ <h1 class="m-0 text-center">{{ translations.events_notFound() }}</h1>
}
}
</div>

<app-event-instances-dialog #instancesDialog />
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -47,6 +48,7 @@ import { EventTimeslotDialogComponent } from '../event-timeslot-dialog/event-tim
CardModule,
CommonModule,
EventFormComponent,
EventInstancesDialogComponent,
EventTimeslotDialogComponent,
FormsModule,
InputGroupAddonModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<p-dialog
[visible]="visible()"
(visibleChange)="visible.set($event)"
[modal]="true"
[resizable]="false"
[draggable]="false"
[header]="translations.events_instancesDialog_title()"
styleClass="max-w-full"
>
<ng-template pTemplate="content">
<div class="flex h-full flex-row gap-8">
@for (x of instances(); track x.timeslot.id) {
<div class="flex flex-col gap-4">
<div
class="sticky -top-2 z-10 flex flex-row items-center gap-2 bg-surface-a px-2 text-lg font-semibold"
>
<span class="i-[mdi--clock-time-four-outline]"></span>
<span>
{{ x.timeslot.time.hour | number: '2.0-0' }}:{{
x.timeslot.time.minute | number: '2.0-0'
}}
</span>
</div>

@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'"
>
<div class="flex flex-row items-center gap-2 font-semibold">
<span class="i-[mdi--pound]"></span>
<span class="grow uppercase">{{ instance.groupCode }}</span>
<p-button
icon="i-[mdi--plus]"
size="small"
[text]="true"
[rounded]="true"
(onClick)="addPlayerPanel.show($event)"
/>
<p-overlayPanel #addPlayerPanel class="hidden" styleClass="p-0">
<ng-template pTemplate="content" styleClass="p-0">
<p-listbox
styleClass="border-none"
[options]="unassignedUsers()[x.timeslot.id]"
optionGroupLabel="label"
optionGroupChildren="items"
optionLabel="alias"
optionValue="id"
[group]="true"
[filter]="true"
(onChange)="
addPlayer(x.timeslot.id, instance.id, $event.value); addPlayerPanel.hide()
"
>
<ng-template let-user pTemplate="listItem">
<app-user-item [user]="user" />
</ng-template>
</p-listbox>
</ng-template>
</p-overlayPanel>
</div>
<div class="flex flex-col gap-1">
@for (playerId of instance.playerIds; track playerId; let pi = $index) {
<div class="flex flex-row items-center gap-2">
<div class="grow truncate">
@if (allUsers()[playerId]; as user) {
{{ user.alias }}
} @else {
&lt;{{ translations.events_timeslot_unknownPlayer() }}&gt;
<span class="opacity-50">({{ playerId }})</span>
}
</div>
<p-button
icon="i-[mdi--pencil]"
size="small"
[text]="true"
[rounded]="true"
(onClick)="editPlayerPanel.show($event)"
/>
<p-overlayPanel #editPlayerPanel class="hidden" styleClass="p-0">
<ng-template pTemplate="content" styleClass="p-0">
<p-listbox
styleClass="border-none"
[options]="moveToInstanceOptions()[x.timeslot.id][instance.id]"
optionLabel="groupCode"
optionValue="id"
(onChange)="
movePlayer(x.timeslot.id, playerId, instance.id, $event.value);
editPlayerPanel.hide()
"
>
<ng-template let-instance pTemplate="listItem">
<div class="flex flex-row items-center gap-2">
@if (instance.id === '<remove>') {
<span class="i-[mdi--delete]"></span>
<span class="grow">{{ translations.shared_remove() }}</span>
} @else {
<span class="i-[mdi--arrow-right-bottom]"></span>
<span class="grow uppercase">{{ instance.groupCode }}</span>
}
</div>
</ng-template>
</p-listbox>
</ng-template>
</p-overlayPanel>
</div>
}
</div>
</div>
}
</div>
}
</div>
</ng-template>

<p-footer>
<p-button
[disabled]="isBusy()"
[text]="true"
[label]="translations.shared_cancel()"
(onClick)="visible.set(false)"
/>
<p-button [disabled]="isBusy()" [label]="translations.shared_save()" (onClick)="submit()" />
</p-footer>
</p-dialog>
Original file line number Diff line number Diff line change
@@ -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: '<remove>',
groupCode: '',
playerIds: [],
};

protected readonly visible = signal(false);
protected readonly instances = signal<EventInstances>([]);
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
}
}
Loading

0 comments on commit 8550f8e

Please sign in to comment.