feat: add waiting-list off-duty and return quick buttons

This commit is contained in:
tothbt committed 2026-11-02 18:01:13 +01:00
1 parent 848e12e2d6
commit 7c5384621a
13 files changed
+450 -9

No files matched your search

+36
View File
@@ -161,6 +161,18 @@
<ion-label>{{ umpire|fullname }}</ion-label>
</div>
<ion-label>{{ umpire|fullname }}</ion-label>
@if (waitingQuickButtonsEnabled()) {
<ion-button
class="waiting-duty-button"
size="small"
fill="solid"
[attr.aria-label]="('moveOffDuty' | translate) + ': ' + (umpire | fullname)"
[attr.title]="'moveOffDuty' | translate"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation(); changeWaitingDuty(umpire, true)">
<ion-icon slot="icon-only" name="exit-outline"></ion-icon>
</ion-button>
}
</div>
}
</ion-list>
@@ -179,6 +191,18 @@
<ion-label>{{ umpire|fullname }}</ion-label>
</div>
<ion-label>{{ umpire|fullname }}</ion-label>
@if (waitingQuickButtonsEnabled()) {
<ion-button
class="waiting-duty-button"
size="small"
fill="solid"
[attr.aria-label]="('moveOffDuty' | translate) + ': ' + (umpire | fullname)"
[attr.title]="'moveOffDuty' | translate"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation(); changeWaitingDuty(umpire, true)">
<ion-icon slot="icon-only" name="exit-outline"></ion-icon>
</ion-button>
}
</div>
}
</ion-list>
@@ -198,6 +222,18 @@
<ion-label>{{ umpire|fullname }}</ion-label>
</div>
<ion-label>{{ umpire|fullname }}</ion-label>
@if (waitingQuickButtonsEnabled() && umpire.returnToQueue) {
<ion-button
class="waiting-duty-button"
size="small"
fill="solid"
[attr.aria-label]="('returnToQueue' | translate) + ': ' + (umpire | fullname)"
[attr.title]="'returnToQueue' | translate"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation(); changeWaitingDuty(umpire, false)">
<ion-icon slot="icon-only" name="enter-outline"></ion-icon>
</ion-button>
}
</div>
}
</ion-list>
+15
View File
@@ -38,3 +38,18 @@
background-color: var(--ion-color-step-150, #d9d9d9);
}
}
.bottom-lists .custom-ion-item {
display: flex;
align-items: center;
gap: 4px;
ion-label {
flex: 1;
min-width: 0;
}
.waiting-duty-button {
flex-shrink: 0;
margin: 0;
}
}
+75
View File
@@ -1,7 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { signal } from '@angular/core';
import { AlertController } from '@ionic/angular';
import { CourtServiceJudge, CourtUmpire, Settings, Umpire } from 'db';
import { WaitingDutyService } from '../services/waiting-duty.service';
import { Tab1Page } from './tab1.page';
import { SettingsService } from '../services/settings-service';
import { UmpireService } from '../services/umpire.service';
@@ -34,6 +36,7 @@ describe('Tab1Page court rotation', () => {
let waitingJudge: jasmine.SpyObj<WaitingServiceJudgesService>;
let umpireCourt: jasmine.SpyObj<CourtUmpireService>;
let judgeCourt: jasmine.SpyObj<CourtServiceJudgeService>;
let duty: jasmine.SpyObj<WaitingDutyService>;
let alerts: jasmine.SpyObj<AlertController>;
beforeEach(() => {
@@ -72,6 +75,9 @@ describe('Tab1Page court rotation', () => {
['save', 'removeByUmpireId'],
{ umpires: courtJudges }
);
duty = jasmine.createSpyObj('WaitingDutyService', ['move', 'moveToRest']);
duty.move.and.resolveTo();
duty.moveToRest.and.resolveTo();
alerts = jasmine.createSpyObj('AlertController', ['create']);
waitingUmpire.getCurrentUmpire.and.resolveTo(official(1));
waitingJudge.getCurrentUmpire.and.resolveTo(official(2));
@@ -85,6 +91,7 @@ describe('Tab1Page court rotation', () => {
judgeCourt.removeByUmpireId.and.resolveTo();
TestBed.configureTestingModule({
providers: [
{ provide: WaitingDutyService, useValue: duty },
{ provide: SettingsService, useValue: { settings } },
{ provide: UmpireService, useValue: { umpires } },
{ provide: WaitingUmpiresService, useValue: waitingUmpire },
@@ -98,6 +105,74 @@ describe('Tab1Page court rotation', () => {
component = fixture.componentInstance;
});
for (const source of [
'list-waiting-umpires',
'list-waiting-service-judges'
]) {
it(`saves the waiting origin when dropped from ${source} to rest`, async () => {
await component.dropToRest({
previousContainer: { id: source },
container: { id: 'list-on-rest' },
item: { data: official(1) }
} as CdkDragDrop<Umpire[]>);
expect(duty.moveToRest).toHaveBeenCalledOnceWith(1);
expect(waitingUmpire.removeByUmpireId).not.toHaveBeenCalled();
expect(waitingJudge.removeByUmpireId).not.toHaveBeenCalled();
});
}
it('moves directly when quick-button alerts are disabled', async () => {
settings.update((value) => ({ ...value, waitingListQuickButtons: true }));
await component.changeWaitingDuty(official(1), true);
expect(duty.move).toHaveBeenCalledOnceWith(1, 'rest');
expect(alerts.create).not.toHaveBeenCalled();
});
it('ignores return actions without a saved origin', async () => {
settings.update((value) => ({
...value,
waitingListQuickButtons: true,
showAlert: true
}));
await component.changeWaitingDuty(official(5), false);
expect(duty.move).not.toHaveBeenCalled();
expect(alerts.create).not.toHaveBeenCalled();
});
it('confirms returning to the saved service-judge queue', async () => {
settings.update((value) => ({
...value,
waitingListQuickButtons: true,
showAlert: true
}));
alerts.create.and.resolveTo({
present: async () => undefined
} as unknown as HTMLIonAlertElement);
await component.changeWaitingDuty(
{ ...official(5), returnToQueue: 'serviceJudge' },
false
);
expect(duty.move).not.toHaveBeenCalled();
const buttons = alerts.create.calls.mostRecent().args[0]?.buttons;
const confirm = buttons?.find(
(button) => typeof button !== 'string' && button.role === 'confirm'
);
if (typeof confirm === 'object' && typeof confirm.handler === 'function')
confirm.handler(undefined);
expect(duty.move).toHaveBeenCalledOnceWith(5, 'serviceJudge');
});
it('hides and ignores quick actions when service judges are disabled', async () => {
settings.update((value) => ({
...value,
waitingListQuickButtons: true,
withServiceJudge: false
}));
expect(component.waitingQuickButtonsEnabled()).toBeFalse();
await component.changeWaitingDuty(official(1), true);
expect(duty.move).not.toHaveBeenCalled();
});
it('waits for confirmation before assigning officials', async () => {
settings.update((value) => ({ ...value, showAlert: true }));
const present = jasmine.createSpy('present').and.resolveTo();
+45 -1
View File
@@ -22,6 +22,7 @@ import {
import { SettingsService } from '../services/settings-service';
import { UmpireService } from '../services/umpire.service';
import { Umpire } from 'db';
import { WaitingDutyService } from '../services/waiting-duty.service';
import { WaitingUmpiresService } from '../services/waiting-umpires.service';
import { WaitingServiceJudgesService } from '../services/waiting-service-judges.service';
import { FullnamePipe } from '../fullname-pipe';
@@ -71,6 +72,8 @@ export class Tab1Page {
addIcons({ exitOutline, enterOutline });
}
readonly waitingDutyService = inject(WaitingDutyService);
readonly settingsService = inject(SettingsService);
readonly umpireService = inject(UmpireService);
readonly waitingUmpireService = inject(WaitingUmpiresService);
@@ -285,12 +288,53 @@ export class Tab1Page {
return this.settings()?.withServiceJudge ?? false;
});
dropToRest(event: CdkDragDrop<Umpire[]>) {
readonly waitingQuickButtonsEnabled = computed(
() =>
this.serviceJudgeEnabled() && !!this.settings()?.waitingListQuickButtons
);
async changeWaitingDuty(umpire: Umpire, toRest: boolean): Promise<void> {
if (!this.waitingQuickButtonsEnabled()) return;
const destination = toRest ? 'rest' : umpire.returnToQueue;
if (!destination) return;
const move = () => this.waitingDutyService.move(umpire.id, destination);
if (!this.settings()?.showAlert) {
await move();
return;
}
const alert = await this.alertController.create({
header: this.translationService.translate('confirmation'),
subHeader: this.translationService.translate(
toRest ? 'confirmMoveOffDuty' : 'confirmReturnToQueue'
),
message: this.fullnamePipe.transform(umpire),
buttons: [
{ text: this.translationService.translate('no'), role: 'cancel' },
{
text: this.translationService.translate('yes'),
role: 'confirm',
handler: () => {
void move();
}
}
]
});
await alert.present();
}
async dropToRest(event: CdkDragDrop<Umpire[]>): Promise<void> {
if (event.previousContainer === event.container) {
return;
} else {
const comingFrom = event.previousContainer.id;
const umpireToMove = event.item.data;
if (
comingFrom === 'list-waiting-umpires' ||
comingFrom === 'list-waiting-service-judges'
) {
await this.waitingDutyService.moveToRest(umpireToMove.id);
return;
}
this.removeFromOriginalPlace(umpireToMove, comingFrom);
}
}