feat(umpires): move umpires between 3 lists by drag-and-drop

This commit is contained in:
2026-05-27 16:54:51 +02:00
parent 03bfc8fc16
commit 71c3733363
10 changed files with 508 additions and 11 deletions
+26 -1
View File
@@ -1,4 +1,5 @@
import Dexie, { type EntityTable } from 'dexie';
import { settings } from 'ionicons/icons';
export interface Umpire {
id: number;
@@ -15,14 +16,38 @@ export interface Court {
order: number;
}
export interface WaitingAsUmpire {
id: number;
umpireId: number;
order: number;
}
export interface WaitingAsServiceJudge {
id: number;
serviceJudgeId: number;
order: number;
}
export interface Settings {
id: number;
withServiceJudge: boolean;
numberOfCourts: number;
}
const db = new Dexie('CourtPilot') as Dexie & {
umpires: EntityTable<Umpire, 'id'>;
courts: EntityTable<Court, 'id'>;
settings: EntityTable<Settings, 'id'>;
waitingUmpires: EntityTable<WaitingAsUmpire, 'id'>;
waitingServiceJudges: EntityTable<WaitingAsServiceJudge, 'id'>;
};
db.version(1).stores({
umpires: '++id, lastName',
courts: '++id, umpireId, serviceJudgeId'
courts: '++id, umpireId, serviceJudgeId',
settings: '++id',
waitingUmpires: '++id, order, umpireId',
waitingServiceJudges: '++id, order, serviceJudgeId'
});
export { db };
+16
View File
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SettingsService } from './settings-service';
describe('SettingsService', () => {
let service: SettingsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SettingsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
+46
View File
@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { db, Settings } from 'db';
import { liveQuery } from 'dexie';
@Injectable({
providedIn: 'root'
})
export class SettingsService {
constructor() {
void this.ensureSettings();
}
/**
* Reactive settings signal
*/
readonly settings = toSignal<Settings | undefined>(
liveQuery(() => db.settings.get(1)),
{ initialValue: undefined }
);
/**
* Update settings
*/
async update(changes: Partial<Omit<Settings, 'id'>>): Promise<void> {
const current = await db.settings.get(1);
if (!current) {
return;
}
await db.settings.update(1, changes);
}
private async ensureSettings() {
const existing = await db.settings.get(1);
if (!existing) {
await db.settings.put({
id: 1,
numberOfCourts: 5,
withServiceJudge: true
});
}
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { WaitingServiceJudgesService } from './waiting-service-judges.service';
describe('WaitingServiceJudgesService', () => {
let service: WaitingServiceJudgesService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(WaitingServiceJudgesService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,70 @@
import { Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { db } from 'db';
import { liveQuery } from 'dexie';
import { from } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class WaitingServiceJudgesService {
/**
* Reactive list sorted by order
*/
readonly waitingServiceJudges = toSignal(
from(liveQuery(() => db.waitingServiceJudges.orderBy('order').toArray())),
{
initialValue: []
}
);
/**
* Add new umpire to queue with max(order) + 1
*/
async add(serviceJudgeId: number): Promise<number> {
return db.transaction('rw', db.waitingServiceJudges, async () => {
const last = await db.waitingServiceJudges.orderBy('order').last();
const nextOrder = last ? last.order + 1 : 1;
return db.waitingServiceJudges.add({
serviceJudgeId,
order: nextOrder
});
});
}
/**
* Remove from queue
*/
async remove(id: number): Promise<void> {
await db.waitingServiceJudges.delete(id);
}
async removeByUmpireId(umpireId: number): Promise<void> {
const item = await db.waitingServiceJudges
.where('serviceJudgeId')
.equals(umpireId)
.first();
if (!item?.id) {
return;
}
await db.waitingServiceJudges.delete(item.id);
}
/**
* Reorder (optional helper)
*/
async updateOrder(id: number, order: number): Promise<void> {
await db.waitingServiceJudges.update(id, { order });
}
/**
* Clear queue
*/
async clear(): Promise<void> {
await db.waitingServiceJudges.clear();
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { WaitingUmpiresService } from './waiting-umpires.service';
describe('WaitingUmpiresService', () => {
let service: WaitingUmpiresService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(WaitingUmpiresService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,70 @@
import { Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { db } from 'db';
import { liveQuery } from 'dexie';
import { from } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class WaitingUmpiresService {
/**
* Reactive list sorted by order
*/
readonly waitingUmpires = toSignal(
from(liveQuery(() => db.waitingUmpires.orderBy('order').toArray())),
{
initialValue: []
}
);
/**
* Add new umpire to queue with max(order) + 1
*/
async add(umpireId: number): Promise<number> {
return db.transaction('rw', db.waitingUmpires, async () => {
const last = await db.waitingUmpires.orderBy('order').last();
const nextOrder = last ? last.order + 1 : 1;
return db.waitingUmpires.add({
umpireId,
order: nextOrder
});
});
}
/**
* Remove from queue
*/
async remove(id: number): Promise<void> {
await db.waitingUmpires.delete(id);
}
async removeByUmpireId(umpireId: number): Promise<void> {
const item = await db.waitingUmpires
.where('umpireId')
.equals(umpireId)
.first();
if (!item?.id) {
return;
}
await db.waitingUmpires.delete(item.id);
}
/**
* Reorder (optional helper)
*/
async updateOrder(id: number, order: number): Promise<void> {
await db.waitingUmpires.update(id, { order });
}
/**
* Clear queue
*/
async clear(): Promise<void> {
await db.waitingUmpires.clear();
}
}
+85 -5
View File
@@ -1,17 +1,97 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Tab 1
</ion-title>
<ion-title> Pályák </ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 1</ion-title>
<ion-title size="large">Pályák</ion-title>
</ion-toolbar>
</ion-header>
<app-explore-container name="Tab 1 page"></app-explore-container>
<ion-grid [fixed]="'fixed'">
<ion-row>
<ion-col [size]="1">Pálya</ion-col>
<ion-col>Játékvezető</ion-col>
<ion-col>Adogatásbíró</ion-col>
</ion-row>
@for (item of [].constructor(settings()?.numberOfCourts); track $index) {
<ion-row>
<ion-col [size]="1">{{ $index + 1 }}.</ion-col>
<ion-col>név1</ion-col>
<ion-col>név2</ion-col>
</ion-row>
}
</ion-grid>
<ion-grid [fixed]="'fixed'" class="ion-margin-top">
<ion-row>
<ion-col [size]="4">Játékvezetők</ion-col>
<ion-col [size]="4">Adogatásbírók</ion-col>
<ion-col [size]="4">Pihenők</ion-col>
</ion-row>
<ion-row>
<ion-col>
<ion-list
[lines]="'none'"
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) {
<ion-item cdkDrag [cdkDragData]="umpire">
<ion-label>{{ umpire|fullname }}</ion-label>
</ion-item>
} @empty {
<ion-item>
<ion-label class="ion-text-center">
<ion-icon [color]="'primary'" name="add"></ion-icon>
</ion-label>
</ion-item>
}
</ion-list>
</ion-col>
<ion-col>
<ion-list
[lines]="'none'"
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) {
<ion-item cdkDrag [cdkDragData]="umpire">
<ion-label>{{ umpire|fullname }}</ion-label>
</ion-item>
} @empty {
<ion-item>
<ion-label class="ion-text-center">
<ion-icon [color]="'primary'" name="add"></ion-icon>
</ion-label>
</ion-item>
}
</ion-list>
</ion-col>
<ion-col>
<ion-list
[lines]="'none'"
cdkDropList
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) {
<ion-item cdkDrag [cdkDragData]="umpire">
<ion-label>{{ umpire|fullname }}</ion-label>
</ion-item>
}
</ion-list>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
+158 -5
View File
@@ -1,13 +1,166 @@
import { Component } from '@angular/core';
import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
import { Component, effect, inject } from '@angular/core';
import {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonGrid,
IonCol,
IonRow,
IonList,
IonItem,
IonLabel,
IonIcon
} from '@ionic/angular/standalone';
import { SettingsService } from '../services/settings-service';
import { UmpireService } from '../services/umpire.service';
import { Umpire, WaitingAsServiceJudge, WaitingAsUmpire } from 'db';
import { WaitingUmpiresService } from '../services/waiting-umpires.service';
import { WaitingServiceJudgesService } from '../services/waiting-service-judges.service';
import { FullnamePipe } from '../fullname-pipe';
import {
CdkDrag,
CdkDragDrop,
CdkDropList,
DragDropModule
} from '@angular/cdk/drag-drop';
import { CommonModule } from '@angular/common';
import { addIcons } from 'ionicons';
import { add } from 'ionicons/icons';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss'],
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent],
imports: [
IonIcon,
IonLabel,
IonItem,
IonList,
IonRow,
IonCol,
IonGrid,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
FullnamePipe,
CdkDropList,
CdkDrag,
CommonModule,
DragDropModule
]
})
export class Tab1Page {
constructor() {}
readonly settingsService = inject(SettingsService);
readonly umpireService = inject(UmpireService);
readonly waitingUmpireService = inject(WaitingUmpiresService);
readonly waitingServiceJudgeService = inject(WaitingServiceJudgesService);
public readonly settings = this.settingsService.settings;
public readonly _umpires = this.umpireService.umpires;
public readonly _waitingUmpires = this.waitingUmpireService.waitingUmpires;
public readonly _waitingServiceJudges =
this.waitingServiceJudgeService.waitingServiceJudges;
public onRest: Umpire[] = [];
public waitingUmpires: Umpire[] = [];
public waitingServiceJudges: Umpire[] = [];
constructor() {
addIcons({ add });
effect(() => {
this.onRest = this._umpires().filter((umpire) => {
return (
!this.isUmpireOnCourt(umpire) &&
!this.isWaitingUmpire(umpire) &&
!this.isWaitingServiceJudge(umpire)
);
});
this.waitingUmpires = this._waitingUmpires()
.map((_wa) => {
return this._umpires().find((u) => u.id === _wa.umpireId);
})
.filter((u) => typeof u !== 'undefined');
this.waitingServiceJudges = this._waitingServiceJudges()
.map((_wsj) => {
return this._umpires().find((u) => u.id === _wsj.serviceJudgeId);
})
.filter((u) => typeof u !== 'undefined');
});
}
private isUmpireOnCourt(umpire: Umpire): boolean {
return false;
}
private isWaitingUmpire(umpire: Umpire): boolean {
return (
typeof this._waitingUmpires().find((wu) => wu.umpireId === umpire.id) !==
'undefined'
);
}
private isWaitingServiceJudge(umpire: Umpire): boolean {
return (
typeof this._waitingServiceJudges().find(
(wsj) => wsj.serviceJudgeId === umpire.id
) !== 'undefined'
);
}
dropToRest(event: CdkDragDrop<Umpire[]>) {
if (event.previousContainer === event.container) {
return;
} else {
const comingFrom = event.previousContainer.id;
const umpireToMove = event.item.data;
if ('list-waiting-service-judges' === comingFrom) {
// Remove from waiting service judges
this.waitingServiceJudgeService.removeByUmpireId(umpireToMove.id);
}
if ('list-waiting-umpires' === comingFrom) {
this.waitingUmpireService.removeByUmpireId(umpireToMove.id);
}
}
}
dropToWaitingServiceJudge(event: CdkDragDrop<Umpire[]>) {
if (event.previousContainer === event.container) {
// TODO
} else {
const comingFrom = event.previousContainer.id;
const umpireToMove = event.item.data;
this.waitingServiceJudgeService.add(umpireToMove.id);
if ('list-waiting-umpires' === comingFrom) {
this.waitingUmpireService.removeByUmpireId(umpireToMove.id);
}
}
}
dropToWaitingUmpire(event: CdkDragDrop<Umpire[]>) {
console.log(
event.container.data,
event.previousContainer.data,
event.previousContainer.id,
event.container.id
);
if (event.previousContainer === event.container) {
// TODO
} else {
const comingFrom = event.previousContainer.id;
const umpireToMove = event.item.data;
this.waitingUmpireService.add(umpireToMove.id);
if ('list-waiting-service-judges' === comingFrom) {
// Remove from waiting service judges
this.waitingServiceJudgeService.removeByUmpireId(umpireToMove.id);
}
}
}
}
+5
View File
@@ -14,5 +14,10 @@
<ion-icon aria-hidden="true" name="square"></ion-icon>
<ion-label>Tab 3</ion-label>
</ion-tab-button>
<ion-tab-button tab="stats" href="/tabs/settings">
<ion-icon aria-hidden="true" name="square"></ion-icon>
<ion-label>Beállítások</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>