From 0c071c062135d5047bf357de5aec936c91e1a1d2 Mon Sep 17 00:00:00 2001 From: Richard Toth Date: Wed, 27 May 2026 21:10:38 +0200 Subject: [PATCH] umpires can be added to courts as umpires (removal also) --- db.ts | 19 +- src/app/fullname-pipe.ts | 5 +- .../court.service.judge.service.spec.ts | 16 ++ .../services/court.service.judge.service.ts | 48 ++++ src/app/services/court.umpire.service.spec.ts | 16 ++ src/app/services/court.umpire.service.ts | 56 ++++ src/app/tab1/tab1.page.html | 95 ++++--- src/app/tab1/tab1.page.scss | 3 + src/app/tab1/tab1.page.ts | 267 ++++++++++++++---- src/theme/variables.scss | 1 + 10 files changed, 436 insertions(+), 90 deletions(-) create mode 100644 src/app/services/court.service.judge.service.spec.ts create mode 100644 src/app/services/court.service.judge.service.ts create mode 100644 src/app/services/court.umpire.service.spec.ts create mode 100644 src/app/services/court.umpire.service.ts diff --git a/db.ts b/db.ts index 77dd081..0761048 100644 --- a/db.ts +++ b/db.ts @@ -1,5 +1,4 @@ import Dexie, { type EntityTable } from 'dexie'; -import { settings } from 'ionicons/icons'; export interface Umpire { id: number; @@ -7,13 +6,19 @@ export interface Umpire { lastName: string; country: string; gender: string; + courtNo?: number; } -export interface Court { +export interface CourtUmpire { id: number; umpireId: number | null; - serviceJudgeId: number | null; - order: number; + courtNo: number; +} + +export interface CourtServiceJudge { + id: number; + umpireId: number | null; + courtNo: number; } export interface WaitingAsUmpire { @@ -36,7 +41,8 @@ export interface Settings { const db = new Dexie('CourtPilot') as Dexie & { umpires: EntityTable; - courts: EntityTable; + courtUmpires: EntityTable; + courtServiceJudges: EntityTable; settings: EntityTable; waitingUmpires: EntityTable; waitingServiceJudges: EntityTable; @@ -44,7 +50,8 @@ const db = new Dexie('CourtPilot') as Dexie & { db.version(1).stores({ umpires: '++id, lastName', - courts: '++id, umpireId, serviceJudgeId', + courtUmpires: '++id, courtNo, umpireId', + courtServiceJudges: '++id, courtNo, umpireId', settings: '++id', waitingUmpires: '++id, order, umpireId', waitingServiceJudges: '++id, order, serviceJudgeId' diff --git a/src/app/fullname-pipe.ts b/src/app/fullname-pipe.ts index a57e9e5..740fc0f 100644 --- a/src/app/fullname-pipe.ts +++ b/src/app/fullname-pipe.ts @@ -6,7 +6,10 @@ import { Umpire } from 'db'; standalone: true }) export class FullnamePipe implements PipeTransform { - transform(value: Umpire, ...args: unknown[]): string { + transform(value: Umpire | undefined, ...args: unknown[]): string { + if (typeof value === 'undefined') { + return ''; + } // TODO: in case of multilang, change here return value.lastName + ' ' + value.firstName; } diff --git a/src/app/services/court.service.judge.service.spec.ts b/src/app/services/court.service.judge.service.spec.ts new file mode 100644 index 0000000..9bd8625 --- /dev/null +++ b/src/app/services/court.service.judge.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CourtServiceJudgeService } from './court.service.judge.service'; + +describe('CourtServiceJudgeService', () => { + let service: CourtServiceJudgeService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CourtServiceJudgeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/court.service.judge.service.ts b/src/app/services/court.service.judge.service.ts new file mode 100644 index 0000000..c52c40c --- /dev/null +++ b/src/app/services/court.service.judge.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { CourtServiceJudge, db } from 'db'; +import { liveQuery } from 'dexie'; +import { from } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class CourtServiceJudgeService { + /** + * All service judges (reactive) + */ + readonly umpires = toSignal( + from(liveQuery(() => db.courtServiceJudges.orderBy('courtNo').toArray())), + { initialValue: [] } + ); + + /** + * Get by court number (reactive) + */ + getByCourtNo(courtNo: number) { + return toSignal( + liveQuery(() => + db.courtServiceJudges.where('courtNo').equals(courtNo).first() + ), + { initialValue: undefined } + ); + } + + /** + * Create / update + */ + async save(item: Omit): Promise { + return db.courtServiceJudges.add(item as CourtServiceJudge); + } + + async update( + id: number, + changes: Partial + ): Promise { + return db.courtServiceJudges.update(id, changes); + } + + async delete(id: number): Promise { + await db.courtServiceJudges.delete(id); + } +} diff --git a/src/app/services/court.umpire.service.spec.ts b/src/app/services/court.umpire.service.spec.ts new file mode 100644 index 0000000..c67be1a --- /dev/null +++ b/src/app/services/court.umpire.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CourtUmpireService } from './court.umpire.service'; + +describe('CourtUmpireService', () => { + let service: CourtUmpireService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CourtUmpireService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/court.umpire.service.ts b/src/app/services/court.umpire.service.ts new file mode 100644 index 0000000..b8eb6a4 --- /dev/null +++ b/src/app/services/court.umpire.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { CourtUmpire, db } from 'db'; +import { liveQuery } from 'dexie'; +import { from } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class CourtUmpireService { + /** + * All court umpires (reactive) + */ + readonly umpires = toSignal( + from(liveQuery(() => db.courtUmpires.orderBy('courtNo').toArray())), + { initialValue: [] } + ); + + /** + * Get by court number (reactive single) + */ + getByCourtNo(courtNo: number) { + return toSignal( + liveQuery(() => db.courtUmpires.where('courtNo').equals(courtNo).first()), + { initialValue: undefined } + ); + } + + /** + * Create / update + */ + async save(item: Omit): Promise { + return db.courtUmpires.add(item as CourtUmpire); + } + + async update(id: number, changes: Partial): Promise { + return db.courtUmpires.update(id, changes); + } + + async delete(id: number): Promise { + await db.courtUmpires.delete(id); + } + + async removeByUmpireId(umpireId: number): Promise { + const item = await db.courtUmpires + .where('umpireId') + .equals(umpireId) + .first(); + + if (!item?.id) { + return; + } + + await db.courtUmpires.delete(item.id); + } +} diff --git a/src/app/tab1/tab1.page.html b/src/app/tab1/tab1.page.html index 3db6976..9a850fb 100644 --- a/src/app/tab1/tab1.page.html +++ b/src/app/tab1/tab1.page.html @@ -4,32 +4,72 @@ - + Pályák - + - Pálya - Játékvezető - Adogatásbíró + + Pálya + + + P + + Játékvezető + Adogatásbíró @for (item of [].constructor(settings()?.numberOfCourts); track $index) { - {{ $index + 1 }}. - név1 - név2 + + + {{ $index + 1 }}. + + + + + @if (umpireByCourt().get($index + 1); as umpire) { + + + {{ umpire | fullname }} + + + } + + + + aaa + } - + - Játékvezetők - Adogatásbírók - Pihenők + + + Játékvezetők + + + + + Adogatásbírók + + + + + Pihenők + + @@ -38,19 +78,12 @@ cdkDropList cdkDropListSortingDisabled id="list-waiting-umpires" - [cdkDropListData]="waitingUmpires" - (cdkDropListDropped)="dropToWaitingUmpire($event)" - [cdkDropListConnectedTo]="['list-on-rest', 'list-waiting-service-judges']"> - @for (umpire of waitingUmpires; track umpire.id) { + [cdkDropListData]="waitingUmpires()" + (cdkDropListDropped)="dropToWaitingUmpire($event)"> + @for (umpire of waitingUmpires(); track umpire.id) { {{ umpire|fullname }} - } @empty { - - - - - } @@ -60,19 +93,12 @@ cdkDropList cdkDropListSortingDisabled id="list-waiting-service-judges" - [cdkDropListData]="waitingServiceJudges" - (cdkDropListDropped)="dropToWaitingServiceJudge($event)" - [cdkDropListConnectedTo]="['list-on-rest', 'list-waiting-umpires']"> - @for (umpire of waitingServiceJudges; track umpire.id) { + [cdkDropListData]="waitingServiceJudges()" + (cdkDropListDropped)="dropToWaitingServiceJudge($event)"> + @for (umpire of waitingServiceJudges(); track umpire.id) { {{ umpire|fullname }} - } @empty { - - - - - } @@ -83,9 +109,8 @@ cdkDropListSortingDisabled id="list-on-rest" (cdkDropListDropped)="dropToRest($event)" - [cdkDropListData]="onRest" - [cdkDropListConnectedTo]="['list-waiting-service-judges', 'list-waiting-umpires']"> - @for (umpire of onRest; track umpire.id) { + [cdkDropListData]="onRest()"> + @for (umpire of onRest(); track umpire.id) { {{ umpire|fullname }} diff --git a/src/app/tab1/tab1.page.scss b/src/app/tab1/tab1.page.scss index e69de29..2cd2297 100644 --- a/src/app/tab1/tab1.page.scss +++ b/src/app/tab1/tab1.page.scss @@ -0,0 +1,3 @@ +.bottom-lists ion-list { + min-height: 64px; +} \ No newline at end of file diff --git a/src/app/tab1/tab1.page.ts b/src/app/tab1/tab1.page.ts index 08afcba..4c11e41 100644 --- a/src/app/tab1/tab1.page.ts +++ b/src/app/tab1/tab1.page.ts @@ -1,4 +1,4 @@ -import { Component, effect, inject } from '@angular/core'; +import { Component, computed, effect, inject } from '@angular/core'; import { IonHeader, IonToolbar, @@ -25,15 +25,14 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { CommonModule } from '@angular/common'; -import { addIcons } from 'ionicons'; -import { add } from 'ionicons/icons'; +import { CourtUmpireService } from '../services/court.umpire.service'; +import { CourtServiceJudgeService } from '../services/court.service.judge.service'; @Component({ selector: 'app-tab1', templateUrl: 'tab1.page.html', styleUrls: ['tab1.page.scss'], imports: [ - IonIcon, IonLabel, IonItem, IonList, @@ -56,63 +55,207 @@ export class Tab1Page { readonly umpireService = inject(UmpireService); readonly waitingUmpireService = inject(WaitingUmpiresService); readonly waitingServiceJudgeService = inject(WaitingServiceJudgesService); + readonly courtUmpireService = inject(CourtUmpireService); + readonly courtServiceJudgeService = inject(CourtServiceJudgeService); - public readonly settings = this.settingsService.settings; - public readonly _umpires = this.umpireService.umpires; - public readonly _waitingUmpires = this.waitingUmpireService.waitingUmpires; - public readonly _waitingServiceJudges = + /** + * Raw signals from services + */ + readonly settings = this.settingsService.settings; + + readonly _umpires = this.umpireService.umpires; + + readonly _waitingUmpires = this.waitingUmpireService.waitingUmpires; + + readonly _waitingServiceJudges = this.waitingServiceJudgeService.waitingServiceJudges; - public onRest: Umpire[] = []; - public waitingUmpires: Umpire[] = []; - public waitingServiceJudges: Umpire[] = []; + readonly _courtUmpires = this.courtUmpireService.umpires; - constructor() { - addIcons({ add }); + readonly _courtServiceJudges = this.courtServiceJudgeService.umpires; - effect(() => { - this.onRest = this._umpires().filter((umpire) => { - return ( - !this.isUmpireOnCourt(umpire) && - !this.isWaitingUmpire(umpire) && - !this.isWaitingServiceJudge(umpire) - ); - }); + /** + * Fast O(1) umpire lookup + */ + readonly umpireMap = computed(() => { + return new Map(this._umpires().map((u) => [u.id, u])); + }); - this.waitingUmpires = this._waitingUmpires() - .map((_wa) => { - return this._umpires().find((u) => u.id === _wa.umpireId); - }) - .filter((u) => typeof u !== 'undefined'); + /** + * Waiting umpire IDs + */ + readonly waitingUmpireIds = computed(() => { + return new Set(this._waitingUmpires().map((w) => w.umpireId)); + }); - this.waitingServiceJudges = this._waitingServiceJudges() - .map((_wsj) => { - return this._umpires().find((u) => u.id === _wsj.serviceJudgeId); - }) - .filter((u) => typeof u !== 'undefined'); + /** + * Waiting service judge IDs + */ + readonly waitingServiceJudgeIds = computed(() => { + return new Set(this._waitingServiceJudges().map((w) => w.serviceJudgeId)); + }); + + /** + * Court umpire IDs + */ + readonly courtUmpireIds = computed(() => { + return new Set( + this._courtUmpires() + .map((c) => c.umpireId) + .filter((id): id is number => id !== null) + ); + }); + + /** + * Court service judge IDs + */ + readonly courtServiceJudgeIds = computed(() => { + return new Set( + this._courtServiceJudges() + .map((c) => c.umpireId) + .filter((id): id is number => id !== null) + ); + }); + + /** + * Umpires waiting as umpire + */ + readonly waitingUmpires = computed(() => { + const umpireMap = this.umpireMap(); + + return this._waitingUmpires() + .map((wu) => umpireMap.get(wu.umpireId)) + .filter((u): u is Umpire => !!u); + }); + + /** + * Umpires waiting as service judge + */ + readonly waitingServiceJudges = computed(() => { + const umpireMap = this.umpireMap(); + + return this._waitingServiceJudges() + .map((wsj) => umpireMap.get(wsj.serviceJudgeId)) + .filter((u): u is Umpire => !!u); + }); + + /** + * Court umpires + */ + readonly courtUmpires = computed(() => { + const umpireMap = this.umpireMap(); + + return this._courtUmpires() + .map((cu) => { + if (cu.umpireId == null) { + return undefined; + } + + return umpireMap.get(cu.umpireId); + }) + .filter((u): u is Umpire => !!u); + }); + + /** + * Court service judges + */ + readonly courtServiceJudges = computed(() => { + const umpireMap = this.umpireMap(); + + return this._courtServiceJudges() + .map((csj) => { + if (csj.umpireId == null) { + return undefined; + } + + return umpireMap.get(csj.umpireId); + }) + .filter((u): u is Umpire => !!u); + }); + + /** + * Umpires resting + */ + readonly onRest = computed(() => { + const waitingUmpireIds = this.waitingUmpireIds(); + + const waitingServiceJudgeIds = this.waitingServiceJudgeIds(); + + const courtUmpireIds = this.courtUmpireIds(); + + const courtServiceJudgeIds = this.courtServiceJudgeIds(); + + return this._umpires().filter((umpire) => { + return ( + !waitingUmpireIds.has(umpire.id) && + !waitingServiceJudgeIds.has(umpire.id) && + !courtUmpireIds.has(umpire.id) && + !courtServiceJudgeIds.has(umpire.id) + ); }); - } + }); - private isUmpireOnCourt(umpire: Umpire): boolean { - return false; - } + /** + * Court -> umpire map + */ + readonly umpireByCourt = computed(() => { + const map = new Map(); - private isWaitingUmpire(umpire: Umpire): boolean { - return ( - typeof this._waitingUmpires().find((wu) => wu.umpireId === umpire.id) !== - 'undefined' - ); - } + const umpireMap = this.umpireMap(); - private isWaitingServiceJudge(umpire: Umpire): boolean { - return ( - typeof this._waitingServiceJudges().find( - (wsj) => wsj.serviceJudgeId === umpire.id - ) !== 'undefined' - ); - } + for (const item of this._courtUmpires()) { + if (item.umpireId == null) { + continue; + } + + const umpire = umpireMap.get(item.umpireId); + + if (!umpire) { + continue; + } + + map.set(item.courtNo, umpire); + } + + return map; + }); + + /** + * Court -> service judge map + */ + readonly serviceJudgeByCourt = computed(() => { + const map = new Map(); + + const umpireMap = this.umpireMap(); + + for (const item of this._courtServiceJudges()) { + if (item.umpireId == null) { + continue; + } + + const umpire = umpireMap.get(item.umpireId); + + if (!umpire) { + continue; + } + + map.set(item.courtNo, umpire); + } + + return map; + }); + + /** + * Court indexes for template + */ + readonly courtIndexes = computed(() => { + const count = this.settings()?.numberOfCourts ?? 0; + + return Array.from({ length: count }, (_, i) => i + 1); + }); dropToRest(event: CdkDragDrop) { + console.log('drop to rest'); if (event.previousContainer === event.container) { return; } else { @@ -126,6 +269,10 @@ export class Tab1Page { if ('list-waiting-umpires' === comingFrom) { this.waitingUmpireService.removeByUmpireId(umpireToMove.id); } + + if (comingFrom.startsWith('court-umpire')) { + this.courtUmpireService.removeByUmpireId(umpireToMove.id); + } } } @@ -140,6 +287,10 @@ export class Tab1Page { if ('list-waiting-umpires' === comingFrom) { this.waitingUmpireService.removeByUmpireId(umpireToMove.id); } + + if (comingFrom.startsWith('court-umpire')) { + this.courtUmpireService.removeByUmpireId(umpireToMove.id); + } } } @@ -161,6 +312,26 @@ export class Tab1Page { // Remove from waiting service judges this.waitingServiceJudgeService.removeByUmpireId(umpireToMove.id); } + + if (comingFrom.startsWith('court-umpire')) { + this.courtUmpireService.removeByUmpireId(umpireToMove.id); + } + } + } + + dropToUmpire(event: CdkDragDrop, courtNo: number) { + // TODO: stop dropping if there is already another umpire + + const comingFrom = event.previousContainer.id; + const umpireToMove = event.item.data; + this.courtUmpireService.save({ umpireId: umpireToMove.id, courtNo }); + + if ('list-waiting-service-judges' === comingFrom) { + this.waitingServiceJudgeService.removeByUmpireId(umpireToMove.id); + } + + if ('list-waiting-umpires' === comingFrom) { + this.waitingUmpireService.removeByUmpireId(umpireToMove.id); } } } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index f8ea4bd..55035c7 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -1,2 +1,3 @@ // For information on how to create your own theme, please refer to: // https://ionicframework.com/docs/theming/ +@import '@ionic/angular/css/palettes/dark.always.css'; \ No newline at end of file