-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add UI for editing event instances
Refs: #24
- Loading branch information
Showing
6 changed files
with
335 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
...nt/src/app/components/events/event-instances-dialog/event-instances-dialog.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
<{{ translations.events_timeslot_unknownPlayer() }}> | ||
<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> |
144 changes: 144 additions & 0 deletions
144
...ient/src/app/components/events/event-instances-dialog/event-instances-dialog.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.