feat(umpires): move umpires between 3 lists by drag-and-drop
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user