fix: apply Angular and Ionic audit improvements
This commit is contained in:
1 parent
9739ea55c6
commit
8dad54fcf1
35 files changed
+1021
-482
No files matched your search
+17
-2
@@ -33,7 +33,23 @@ Pages are standalone Angular components and directly import the Ionic standalone
|
||||
|
||||
Persistence services use Dexie's `liveQuery()` and Angular's `toSignal()` to expose reactive data. Pages derive lookup maps, ID sets, filtered lists, and other display state with `computed()`.
|
||||
|
||||
`Tab1Page` currently coordinates multi-service operations such as moving officials between waiting queues and courts. These operations span multiple Dexie tables but are not currently wrapped in database transactions.
|
||||
`CourtRotationService` coordinates drag-and-drop moves, court assignments, and
|
||||
rotations inside Dexie transactions. It rechecks current persisted state before
|
||||
writing, so repeated actions or stale confirmations cannot assign the same queue
|
||||
head twice. Confirmation actions also verify that the role configuration still
|
||||
matches the officials shown in the alert. Pages handle presentation and delegate
|
||||
the state changes to services.
|
||||
|
||||
`SettingsService` validates positive integer court counts and atomically updates
|
||||
settings with assignment cleanup. `UmpireService` atomically deletes an official's
|
||||
duty records and closes queue gaps when deleting that official. Roster forms await
|
||||
persistence before dismissing their modal and keep the draft if saving fails.
|
||||
|
||||
The Ionic 9 `@ionic/angular` entry point exports standalone components. Root and
|
||||
tab outlet hosts explicitly use eager change detection so plain-field form state
|
||||
continues to update under Angular 22. Translation and full-name pipes remain
|
||||
impure because their results also depend on the selected language, beyond their
|
||||
explicit pipe arguments.
|
||||
|
||||
## Persistence model
|
||||
|
||||
@@ -137,5 +153,4 @@ Operations that move an official between states should preserve exclusivity even
|
||||
|
||||
## Known architectural questions
|
||||
|
||||
- Should moves spanning multiple tables use Dexie transactions to prevent partial updates?
|
||||
- Should domain entities and database setup remain in root-level `db.ts`, or move under `src/app/`?
|
||||
@@ -0,0 +1,93 @@
|
||||
# Angular and Ionic code audit
|
||||
|
||||
Reviewed on 2026-11-04 against the installed Angular 22 and Ionic 9 packages.
|
||||
Scope: application components, templates, styles, services, localization,
|
||||
bootstrap, routes, TypeScript configuration, and lint rules.
|
||||
|
||||
## Corrections
|
||||
|
||||
- Moved court and queue state changes from page orchestration into
|
||||
`CourtRotationService`. Moves and rotations now use transactions, await writes,
|
||||
check persisted court occupancy, and reject stale confirmations. A failed write
|
||||
rolls back the entire operation.
|
||||
- Fixed rotation without confirmation when service judges are disabled. The lone
|
||||
umpire returns to the umpire queue.
|
||||
- Moved court-count and service-judge cleanup into `SettingsService` transactions.
|
||||
Court counts must be positive safe integers. Court count changes commit after
|
||||
the input is confirmed rather than deleting assignments during each keystroke.
|
||||
- Made roster deletion remove dependent duty records and normalize remaining
|
||||
queue positions in the same transaction. Clearing the roster clears duty too.
|
||||
- Made queue insertion/reordering and deletion/renumbering atomic. Queue heads
|
||||
use the lowest numeric order, including when the first order is greater than one.
|
||||
- Replaced method-created `toSignal()` court queries with `computed()` lookups
|
||||
derived from existing service signals. Callers no longer need an injection
|
||||
context or create a new database subscription for every lookup.
|
||||
- Kept defensive copies of caller-owned records before Dexie inserts, so generated
|
||||
IDs do not mutate form or caller state. Removed unnecessary entity casts.
|
||||
- Made roster saves finish before modal dismissal. The form prevents duplicate
|
||||
saves, rejects blank names, trims names and country, and preserves the draft
|
||||
with a translated error alert when saving fails. Modal presentation uses its
|
||||
Angular view reference instead of a global trigger ID.
|
||||
- Replaced the court loop's array-constructor migration expression with the
|
||||
existing computed court indexes and stable court-number tracking.
|
||||
- Corrected Ionic grid boolean bindings, removed redundant literal property
|
||||
bindings and an empty disabled binding, and applied the empty-court drag
|
||||
predicate consistently to both roles.
|
||||
- Localized the remaining hardcoded roster heading, added names for icon-only
|
||||
actions, allowed browser zoom, and synchronized the document language with
|
||||
the selected UI language.
|
||||
- Exposed language state as a readonly signal and rejected inherited object keys
|
||||
such as `toString` as saved language values.
|
||||
- Replaced alert styles coupled to generated Ionic classes with the public host
|
||||
class and CSS variables, with a viewport-aware minimum width.
|
||||
- Added bootstrap error handling and moved the desktop platform lookup inside
|
||||
the updater's error boundary.
|
||||
- Removed unused dependencies, imports, pipe arguments, and non-null assertions;
|
||||
added `readonly` where appropriate and typed Ionic events and CDK drag data.
|
||||
- Enabled lint rules for unused variables and explicit `any`. Removed the blanket
|
||||
suppression of optional-chain and nullish-coalescing compiler diagnostics.
|
||||
- Formatted changed files with the repository Prettier configuration and updated
|
||||
the architecture documentation.
|
||||
|
||||
## Conventions retained
|
||||
|
||||
- Ionic 9 exports standalone components from `@ionic/angular`; no import-path
|
||||
migration is needed. The installed package does not export the old
|
||||
`@ionic/angular/standalone` entry point.
|
||||
- Eager change detection remains explicit on the root and Ionic routed components.
|
||||
Ionic outlet ancestors must continue checking pages that use plain-field form
|
||||
state. See [Ionic page lifecycle](https://ionicframework.com/docs/angular/lifecycle).
|
||||
- Existing standalone components, lazy routes, `inject()`, strict typing, reactive
|
||||
service signals, and computed page state already follow the project's approach.
|
||||
See [Ionic standalone build options](https://ionicframework.com/docs/angular/build-options)
|
||||
and [Angular component guidance](https://github.com/angular/angular/blob/main/packages/core/resources/best-practices.md).
|
||||
- The translation and full-name pipes remain impure: their output depends on a
|
||||
language signal in addition to the explicit pipe arguments.
|
||||
- Template-driven forms remain appropriate for the small roster form. This audit
|
||||
does not require a form-framework migration.
|
||||
- No database schema, release versions, dependencies, or tournament rules changed.
|
||||
|
||||
## Validation
|
||||
|
||||
Final checks, using Node `lts/jod`:
|
||||
|
||||
- `npm run lint` — passed.
|
||||
- `npm run build` — passed, including strict template checking with diagnostic
|
||||
suppressions removed.
|
||||
- `CHROME_BIN=/snap/bin/chromium npm test -- --watch=false --browsers=ChromeHeadless`
|
||||
— all 102 tests passed. Karma and Chromium ran outside the sandbox because the
|
||||
sandbox cannot bind Karma's server port.
|
||||
- Focused roster tests using `--include=src/app/tab2/tab2.page.spec.ts` — all four
|
||||
passed before the final full-suite run.
|
||||
- Prettier on touched files and `git diff --check` — passed.
|
||||
|
||||
Regression coverage includes concurrent assignment attempts, failed rotation
|
||||
rollback, stale confirmations, FIFO ordering with gaps, state exclusivity,
|
||||
settings cleanup, cascade deletion, readonly language behavior, name localization,
|
||||
and roster save failure and duplicate-submit behavior.
|
||||
|
||||
Desktop installation, updater downloads, native platform builds, and manual
|
||||
end-to-end drag interaction were not exercised by this frontend audit.
|
||||
The existing Karma builder and Zone.js/Jasmine integration emit upstream
|
||||
deprecation messages; migrating the runner or dependency versions is separate
|
||||
from these application fixes.
|
||||
+9
-1
@@ -1,11 +1,13 @@
|
||||
const { defineConfig } = require('eslint/config');
|
||||
const angular = require('angular-eslint');
|
||||
const typescript = require('@typescript-eslint/eslint-plugin');
|
||||
|
||||
module.exports = defineConfig(
|
||||
{ ignores: ['projects/**', 'www/**', 'coverage/**'] },
|
||||
{
|
||||
files: ['src/**/*.ts', 'db.ts'],
|
||||
extends: angular.configs.tsRecommended,
|
||||
plugins: { '@typescript-eslint': typescript },
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.eslint.json'],
|
||||
@@ -14,8 +16,14 @@ module.exports = defineConfig(
|
||||
},
|
||||
processor: angular.processInlineTemplates,
|
||||
rules: {
|
||||
// Preserve existing Ionic page updates while retaining Zone.js.
|
||||
// Ionic outlets and plain-field forms need eager change detection.
|
||||
// https://ionicframework.com/docs/angular/lifecycle
|
||||
'@angular-eslint/prefer-on-push-component-change-detection': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_' }
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@angular-eslint/component-class-suffix': [
|
||||
'error',
|
||||
{ suffixes: ['Page', 'Component'] }
|
||||
|
||||
@@ -26,7 +26,7 @@ export class AppComponent {
|
||||
readonly themeService = inject(ThemeService);
|
||||
|
||||
constructor() {
|
||||
this.checkForUpdates();
|
||||
void this.checkForUpdates();
|
||||
}
|
||||
|
||||
private async checkForUpdates(): Promise<void> {
|
||||
@@ -34,12 +34,9 @@ export class AppComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// Snap handles updates on Linux.
|
||||
if (platform() === 'linux') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Snap handles updates on Linux.
|
||||
if (platform() === 'linux') return;
|
||||
const update = await check();
|
||||
|
||||
if (update) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TranslationService } from './i18n/translation.service';
|
||||
export class FullnamePipe implements PipeTransform {
|
||||
readonly translationService = inject(TranslationService);
|
||||
|
||||
transform(value: Umpire | undefined, ...args: unknown[]): string {
|
||||
transform(value: Umpire | undefined): string {
|
||||
if (typeof value === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TranslationService } from './translation.service';
|
||||
import { TranslatePipe } from './translation.pipe';
|
||||
import { FullnamePipe } from '../fullname-pipe';
|
||||
|
||||
describe('TranslationService', () => {
|
||||
let stored: string | null;
|
||||
let documentLanguage: string;
|
||||
beforeEach(() => {
|
||||
documentLanguage = document.documentElement.lang;
|
||||
stored = localStorage.getItem('language');
|
||||
localStorage.setItem('language', 'en');
|
||||
});
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
document.documentElement.lang = documentLanguage;
|
||||
if (stored === null) localStorage.removeItem('language');
|
||||
else localStorage.setItem('language', stored);
|
||||
});
|
||||
|
||||
it('ignores inherited object keys in saved language values', () => {
|
||||
localStorage.setItem('language', 'toString');
|
||||
const service = TestBed.inject(TranslationService);
|
||||
expect(['en', 'hu']).toContain(service.language());
|
||||
expect(typeof service.translate('court')).toBe('string');
|
||||
});
|
||||
|
||||
it('updates translations and name order when the language changes', () => {
|
||||
const service = TestBed.inject(TranslationService);
|
||||
const translate = TestBed.runInInjectionContext(() => new TranslatePipe());
|
||||
const fullname = TestBed.runInInjectionContext(() => new FullnamePipe());
|
||||
const official = {
|
||||
id: 1,
|
||||
firstName: 'Alex',
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: 'M'
|
||||
};
|
||||
expect(translate.transform('court')).toBe('Court');
|
||||
expect(fullname.transform(official)).toBe('Alex Smith');
|
||||
service.setLanguage('hu');
|
||||
expect(translate.transform('court')).toBe('Pálya');
|
||||
expect(fullname.transform(official)).toBe('Smith Alex');
|
||||
expect(localStorage.getItem('language')).toBe('hu');
|
||||
expect(document.documentElement.lang).toBe('hu');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import {
|
||||
translations,
|
||||
Language,
|
||||
@@ -10,12 +11,21 @@ import {
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TranslationService {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly storageKey = 'language';
|
||||
|
||||
readonly language = signal<Language>(this.getInitialLanguage());
|
||||
private readonly selectedLanguage = signal<Language>(
|
||||
this.getInitialLanguage()
|
||||
);
|
||||
readonly language = this.selectedLanguage.asReadonly();
|
||||
|
||||
constructor() {
|
||||
this.document.documentElement.lang = this.language();
|
||||
}
|
||||
|
||||
setLanguage(language: Language): void {
|
||||
this.language.set(language);
|
||||
this.selectedLanguage.set(language);
|
||||
this.document.documentElement.lang = language;
|
||||
localStorage.setItem(this.storageKey, language);
|
||||
}
|
||||
|
||||
@@ -32,13 +42,13 @@ export class TranslationService {
|
||||
private getInitialLanguage(): Language {
|
||||
const stored = localStorage.getItem(this.storageKey);
|
||||
|
||||
if (stored && stored in translations) {
|
||||
return stored as Language;
|
||||
if (stored === 'en' || stored === 'hu') {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const browserLanguage = navigator.language.split('-')[0] as Language;
|
||||
const browserLanguage = navigator.language.split('-')[0];
|
||||
|
||||
if (browserLanguage in translations) {
|
||||
if (browserLanguage === 'en' || browserLanguage === 'hu') {
|
||||
return browserLanguage;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { language, male, settings } from 'ionicons/icons';
|
||||
import { noop } from 'rxjs';
|
||||
|
||||
export const translations = {
|
||||
en: {
|
||||
courts: 'Courts',
|
||||
@@ -27,6 +24,7 @@ export const translations = {
|
||||
newCourtOfficial: 'New Court Official',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
saveFailed: 'Could not save the court official. Please try again.',
|
||||
firstName: 'First Name',
|
||||
lastName: 'Last Name',
|
||||
country: 'Country',
|
||||
@@ -88,6 +86,7 @@ export const translations = {
|
||||
newCourtOfficial: 'Új Tisztségviselő',
|
||||
cancel: 'Mégse',
|
||||
save: 'Mentés',
|
||||
saveFailed: 'Nem sikerült menteni a tisztségviselőt. Próbáld újra.',
|
||||
firstName: 'Keresztnév',
|
||||
lastName: 'Vezetéknév',
|
||||
country: 'Ország',
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { db } from 'db';
|
||||
import { CourtRotationService } from './court-rotation.service';
|
||||
import { WaitingUmpiresService } from './waiting-umpires.service';
|
||||
|
||||
describe('CourtRotationService', () => {
|
||||
let service: CourtRotationService;
|
||||
const clear = async () => {
|
||||
await Promise.all([
|
||||
db.settings.clear(),
|
||||
db.umpires.clear(),
|
||||
db.waitingUmpires.clear(),
|
||||
db.waitingServiceJudges.clear(),
|
||||
db.courtUmpires.clear(),
|
||||
db.courtServiceJudges.clear()
|
||||
]);
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await clear();
|
||||
await db.settings.put({
|
||||
id: 1,
|
||||
numberOfCourts: 3,
|
||||
withServiceJudge: true,
|
||||
showAlert: false
|
||||
});
|
||||
await db.umpires.bulkPut(
|
||||
[1, 2, 3].map((id) => ({
|
||||
id,
|
||||
firstName: `Official ${id}`,
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: 'M'
|
||||
}))
|
||||
);
|
||||
service = TestBed.inject(CourtRotationService);
|
||||
});
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await clear();
|
||||
});
|
||||
|
||||
it('assigns the lowest orders even when the queues do not start at one', async () => {
|
||||
await db.waitingUmpires.bulkAdd([
|
||||
{ umpireId: 1, order: 3 },
|
||||
{ umpireId: 3, order: 4 }
|
||||
]);
|
||||
await db.waitingServiceJudges.add({ serviceJudgeId: 2, order: 8 });
|
||||
await service.assignNext(1, 1, 2);
|
||||
expect(
|
||||
(await db.courtUmpires.toArray()).map((item) => item.umpireId)
|
||||
).toEqual([1]);
|
||||
expect(
|
||||
(await db.courtServiceJudges.toArray()).map((item) => item.umpireId)
|
||||
).toEqual([2]);
|
||||
expect(
|
||||
(await db.waitingUmpires.toArray()).map((item) => [
|
||||
item.umpireId,
|
||||
item.order
|
||||
])
|
||||
).toEqual([[3, 1]]);
|
||||
expect(await db.waitingServiceJudges.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('serializes repeated assignments so officials cannot occupy two courts', async () => {
|
||||
await db.waitingUmpires.add({ umpireId: 1, order: 1 });
|
||||
await db.waitingServiceJudges.add({ serviceJudgeId: 2, order: 1 });
|
||||
await Promise.all([
|
||||
service.assignNext(1, 1, 2),
|
||||
service.assignNext(2, 1, 2)
|
||||
]);
|
||||
expect(await db.courtUmpires.count()).toBe(1);
|
||||
expect(await db.courtServiceJudges.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('ignores stale confirmations and occupied courts', async () => {
|
||||
await db.waitingUmpires.add({ umpireId: 3, order: 1 });
|
||||
await db.waitingServiceJudges.add({ serviceJudgeId: 2, order: 1 });
|
||||
await service.assignNext(1, 1, 2);
|
||||
expect(await db.courtUmpires.count()).toBe(0);
|
||||
await db.courtUmpires.add({ courtNo: 1, umpireId: 1 });
|
||||
await service.assignNext(1, 3, 2);
|
||||
expect(await db.courtUmpires.count()).toBe(1);
|
||||
expect(await db.waitingUmpires.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('rotates both officials into the opposite queues', async () => {
|
||||
await db.courtUmpires.add({ umpireId: 1, courtNo: 1 });
|
||||
await db.courtServiceJudges.add({ umpireId: 2, courtNo: 1 });
|
||||
await service.rotate(1, 1, 2);
|
||||
expect(await db.courtUmpires.count()).toBe(0);
|
||||
expect(await db.courtServiceJudges.count()).toBe(0);
|
||||
expect((await db.waitingUmpires.toArray())[0].umpireId).toBe(2);
|
||||
expect((await db.waitingServiceJudges.toArray())[0].serviceJudgeId).toBe(1);
|
||||
});
|
||||
|
||||
it('returns a lone umpire when service judges are disabled', async () => {
|
||||
await db.settings.update(1, { withServiceJudge: false });
|
||||
await db.courtUmpires.add({ umpireId: 1, courtNo: 1 });
|
||||
await service.rotate(1, 1);
|
||||
expect(await db.courtUmpires.count()).toBe(0);
|
||||
expect((await db.waitingUmpires.toArray())[0].umpireId).toBe(1);
|
||||
});
|
||||
|
||||
it('rolls back court removal if requeuing fails', async () => {
|
||||
await db.settings.update(1, { withServiceJudge: false });
|
||||
await db.courtUmpires.add({ umpireId: 1, courtNo: 1 });
|
||||
spyOn(TestBed.inject(WaitingUmpiresService), 'add').and.rejectWith(
|
||||
new Error('write failed')
|
||||
);
|
||||
await expectAsync(service.rotate(1, 1)).toBeRejectedWithError(
|
||||
'write failed'
|
||||
);
|
||||
expect(await db.courtUmpires.count()).toBe(1);
|
||||
expect(await db.waitingUmpires.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('moves and reorders officials without duplicating their states', async () => {
|
||||
await db.waitingUmpires.bulkAdd([
|
||||
{ umpireId: 1, order: 1 },
|
||||
{ umpireId: 2, order: 2 }
|
||||
]);
|
||||
await service.moveToQueue(2, 'umpire', 1);
|
||||
expect(
|
||||
(await db.waitingUmpires.orderBy('order').toArray()).map(
|
||||
(item) => item.umpireId
|
||||
)
|
||||
).toEqual([2, 1]);
|
||||
await service.moveToQueue(1, 'serviceJudge', 1);
|
||||
await service.moveToCourt(1, 'umpire', 1);
|
||||
expect(await db.waitingServiceJudges.count()).toBe(0);
|
||||
expect(
|
||||
(await db.waitingUmpires.toArray()).map((item) => item.umpireId)
|
||||
).toEqual([2]);
|
||||
await service.moveToRest(1);
|
||||
expect(await db.courtUmpires.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('does not move to an occupied or invalid court or a disabled role', async () => {
|
||||
await db.waitingUmpires.add({ umpireId: 1, order: 1 });
|
||||
await db.courtUmpires.add({ umpireId: 2, courtNo: 1 });
|
||||
await service.moveToCourt(1, 'umpire', 1);
|
||||
await service.moveToCourt(1, 'umpire', 4);
|
||||
await db.settings.update(1, { withServiceJudge: false });
|
||||
await service.moveToQueue(1, 'serviceJudge', 1);
|
||||
await service.moveToCourt(1, 'serviceJudge', 2);
|
||||
expect(await db.waitingUmpires.count()).toBe(1);
|
||||
expect(await db.waitingServiceJudges.count()).toBe(0);
|
||||
expect(await db.courtUmpires.count()).toBe(1);
|
||||
expect(await db.courtServiceJudges.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { db, WaitingRole } from 'db';
|
||||
import { WaitingUmpiresService } from './waiting-umpires.service';
|
||||
import { WaitingServiceJudgesService } from './waiting-service-judges.service';
|
||||
|
||||
/** Coordinates state changes atomically; pages only supply the requested action. */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CourtRotationService {
|
||||
private readonly umpires = inject(WaitingUmpiresService);
|
||||
private readonly judges = inject(WaitingServiceJudgesService);
|
||||
|
||||
private transaction(action: () => Promise<void>): Promise<void> {
|
||||
return db.transaction(
|
||||
'rw',
|
||||
[
|
||||
db.settings,
|
||||
db.umpires,
|
||||
db.waitingUmpires,
|
||||
db.waitingServiceJudges,
|
||||
db.courtUmpires,
|
||||
db.courtServiceJudges
|
||||
],
|
||||
action
|
||||
);
|
||||
}
|
||||
|
||||
private async removeOfficial(id: number): Promise<void> {
|
||||
await db.waitingUmpires.where('umpireId').equals(id).delete();
|
||||
await db.waitingServiceJudges.where('serviceJudgeId').equals(id).delete();
|
||||
await db.courtUmpires.where('umpireId').equals(id).delete();
|
||||
await db.courtServiceJudges.where('umpireId').equals(id).delete();
|
||||
await this.umpires.reinitOrders();
|
||||
await this.judges.reinitOrders();
|
||||
await db.umpires.update(id, { returnToQueue: undefined });
|
||||
}
|
||||
|
||||
async moveToQueue(
|
||||
id: number,
|
||||
role: WaitingRole,
|
||||
position: number
|
||||
): Promise<void> {
|
||||
await this.transaction(async () => {
|
||||
if (!(await db.umpires.get(id))) return;
|
||||
if (
|
||||
role === 'serviceJudge' &&
|
||||
!(await db.settings.get(1))?.withServiceJudge
|
||||
)
|
||||
return;
|
||||
await this.removeOfficial(id);
|
||||
if (role === 'umpire') await this.umpires.add(id, position);
|
||||
else await this.judges.add(id, position);
|
||||
});
|
||||
}
|
||||
|
||||
async moveToRest(id: number): Promise<void> {
|
||||
await this.transaction(async () => {
|
||||
await this.removeOfficial(id);
|
||||
});
|
||||
}
|
||||
|
||||
async moveToCourt(
|
||||
id: number,
|
||||
role: WaitingRole,
|
||||
courtNo: number
|
||||
): Promise<void> {
|
||||
await this.transaction(async () => {
|
||||
const settings = await db.settings.get(1);
|
||||
if (
|
||||
!settings ||
|
||||
!Number.isInteger(courtNo) ||
|
||||
courtNo < 1 ||
|
||||
courtNo > settings.numberOfCourts ||
|
||||
(role === 'serviceJudge' && !settings.withServiceJudge) ||
|
||||
!(await db.umpires.get(id))
|
||||
)
|
||||
return;
|
||||
const table = role === 'umpire' ? db.courtUmpires : db.courtServiceJudges;
|
||||
if (await table.where('courtNo').equals(courtNo).count()) return;
|
||||
await this.removeOfficial(id);
|
||||
await table.add({ umpireId: id, courtNo });
|
||||
});
|
||||
}
|
||||
|
||||
async assignNext(
|
||||
courtNo: number,
|
||||
expectedUmpireId: number,
|
||||
expectedJudgeId?: number
|
||||
): Promise<void> {
|
||||
await this.transaction(async () => {
|
||||
const settings = await db.settings.get(1);
|
||||
if (
|
||||
!settings ||
|
||||
settings.withServiceJudge !== (expectedJudgeId !== undefined) ||
|
||||
!Number.isInteger(courtNo) ||
|
||||
courtNo < 1 ||
|
||||
courtNo > settings.numberOfCourts
|
||||
)
|
||||
return;
|
||||
if (
|
||||
(await db.courtUmpires.where('courtNo').equals(courtNo).count()) ||
|
||||
(await db.courtServiceJudges.where('courtNo').equals(courtNo).count())
|
||||
)
|
||||
return;
|
||||
const umpire = await this.umpires.getCurrentUmpire();
|
||||
const judge = settings.withServiceJudge
|
||||
? await this.judges.getCurrentUmpire()
|
||||
: undefined;
|
||||
// A confirmation must never assign different officials after the queues change.
|
||||
if (
|
||||
!umpire ||
|
||||
umpire.id !== expectedUmpireId ||
|
||||
(settings.withServiceJudge &&
|
||||
(!judge || judge.id !== expectedJudgeId || judge.id === umpire.id))
|
||||
)
|
||||
return;
|
||||
await this.removeOfficial(umpire.id);
|
||||
if (judge) await this.removeOfficial(judge.id);
|
||||
await db.courtUmpires.add({ courtNo, umpireId: umpire.id });
|
||||
if (judge)
|
||||
await db.courtServiceJudges.add({ courtNo, umpireId: judge.id });
|
||||
});
|
||||
}
|
||||
|
||||
async rotate(
|
||||
courtNo: number,
|
||||
expectedUmpireId: number,
|
||||
expectedJudgeId?: number
|
||||
): Promise<void> {
|
||||
await this.transaction(async () => {
|
||||
const settings = await db.settings.get(1);
|
||||
if (
|
||||
!settings ||
|
||||
settings.withServiceJudge !== (expectedJudgeId !== undefined)
|
||||
)
|
||||
return;
|
||||
const umpire = await db.courtUmpires
|
||||
.where('courtNo')
|
||||
.equals(courtNo)
|
||||
.first();
|
||||
const judge = settings.withServiceJudge
|
||||
? await db.courtServiceJudges.where('courtNo').equals(courtNo).first()
|
||||
: undefined;
|
||||
if (
|
||||
!umpire ||
|
||||
umpire.umpireId !== expectedUmpireId ||
|
||||
(settings.withServiceJudge &&
|
||||
(!judge ||
|
||||
judge.umpireId !== expectedJudgeId ||
|
||||
judge.umpireId === umpire.umpireId))
|
||||
)
|
||||
return;
|
||||
if (
|
||||
!(await db.umpires.get(expectedUmpireId)) ||
|
||||
(judge?.umpireId != null && !(await db.umpires.get(judge.umpireId)))
|
||||
)
|
||||
return;
|
||||
await this.removeOfficial(expectedUmpireId);
|
||||
if (judge?.umpireId != null) {
|
||||
await this.removeOfficial(judge.umpireId);
|
||||
await this.umpires.add(judge.umpireId);
|
||||
await this.judges.add(expectedUmpireId);
|
||||
} else {
|
||||
await this.umpires.add(expectedUmpireId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ describe('CourtServiceJudgeService persistence', () => {
|
||||
await db.courtServiceJudges.clear();
|
||||
});
|
||||
|
||||
it('creates a reactive court lookup outside an injection context', () => {
|
||||
const assignment = service.getByCourtNo(1);
|
||||
expect(assignment()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('saves an assignment and returns its generated ID', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 2 });
|
||||
expect(await db.courtServiceJudges.get(id)).toEqual({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { computed, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { CourtServiceJudge, db } from 'db';
|
||||
import { liveQuery } from 'dexie';
|
||||
@@ -20,11 +20,8 @@ export class CourtServiceJudgeService {
|
||||
* Get by court number (reactive)
|
||||
*/
|
||||
getByCourtNo(courtNo: number) {
|
||||
return toSignal<CourtServiceJudge | undefined>(
|
||||
liveQuery(() =>
|
||||
db.courtServiceJudges.where('courtNo').equals(courtNo).first()
|
||||
),
|
||||
{ initialValue: undefined }
|
||||
return computed(() =>
|
||||
this.umpires().find((item) => item.courtNo === courtNo)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +29,7 @@ export class CourtServiceJudgeService {
|
||||
* Create / update
|
||||
*/
|
||||
async save(item: Omit<CourtServiceJudge, 'id'>): Promise<number> {
|
||||
return db.courtServiceJudges.add(item as CourtServiceJudge);
|
||||
return db.courtServiceJudges.add({ ...item });
|
||||
}
|
||||
|
||||
async update(
|
||||
|
||||
@@ -15,6 +15,11 @@ describe('CourtUmpireService persistence', () => {
|
||||
await db.courtUmpires.clear();
|
||||
});
|
||||
|
||||
it('creates a reactive court lookup outside an injection context', () => {
|
||||
const assignment = service.getByCourtNo(1);
|
||||
expect(assignment()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('saves an assignment and returns its generated ID', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 2 });
|
||||
expect(await db.courtUmpires.get(id)).toEqual({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { computed, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { CourtUmpire, db } from 'db';
|
||||
import { liveQuery } from 'dexie';
|
||||
@@ -20,9 +20,8 @@ export class CourtUmpireService {
|
||||
* Get by court number (reactive single)
|
||||
*/
|
||||
getByCourtNo(courtNo: number) {
|
||||
return toSignal<CourtUmpire | undefined>(
|
||||
liveQuery(() => db.courtUmpires.where('courtNo').equals(courtNo).first()),
|
||||
{ initialValue: undefined }
|
||||
return computed(() =>
|
||||
this.umpires().find((item) => item.courtNo === courtNo)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +29,7 @@ export class CourtUmpireService {
|
||||
* Create / update
|
||||
*/
|
||||
async save(item: Omit<CourtUmpire, 'id'>): Promise<number> {
|
||||
return db.courtUmpires.add(item as CourtUmpire);
|
||||
return db.courtUmpires.add({ ...item });
|
||||
}
|
||||
|
||||
async update(id: number, changes: Partial<CourtUmpire>): Promise<number> {
|
||||
|
||||
@@ -1,16 +1,68 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { db } from 'db';
|
||||
import { SettingsService } from './settings-service';
|
||||
|
||||
describe('SettingsService', () => {
|
||||
let service: SettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
const clear = async () => {
|
||||
await Promise.all([
|
||||
db.settings.clear(),
|
||||
db.courtUmpires.clear(),
|
||||
db.courtServiceJudges.clear(),
|
||||
db.waitingServiceJudges.clear()
|
||||
]);
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await clear();
|
||||
service = TestBed.inject(SettingsService);
|
||||
await service.update({ numberOfCourts: 5, withServiceJudge: true });
|
||||
});
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await clear();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
it('initializes defaults before accepting an immediate update', async () => {
|
||||
expect(await db.settings.get(1)).toEqual({
|
||||
id: 1,
|
||||
numberOfCourts: 5,
|
||||
withServiceJudge: true,
|
||||
showAlert: true,
|
||||
waitingListQuickButtons: false
|
||||
});
|
||||
});
|
||||
|
||||
for (const count of [0, -1, 1.5, NaN, Infinity]) {
|
||||
it(`rejects invalid court count ${count} without removing assignments`, async () => {
|
||||
await db.courtUmpires.add({ courtNo: 5, umpireId: 1 });
|
||||
await service.update({ numberOfCourts: count });
|
||||
expect((await db.settings.get(1))?.numberOfCourts).toBe(5);
|
||||
expect(await db.courtUmpires.count()).toBe(1);
|
||||
});
|
||||
}
|
||||
|
||||
it('removes only assignments on eliminated courts', async () => {
|
||||
await db.courtUmpires.bulkAdd([
|
||||
{ courtNo: 1, umpireId: 1 },
|
||||
{ courtNo: 5, umpireId: 2 }
|
||||
]);
|
||||
await db.courtServiceJudges.add({ courtNo: 5, umpireId: 3 });
|
||||
await service.update({ numberOfCourts: 2 });
|
||||
expect(
|
||||
(await db.courtUmpires.toArray()).map((item) => item.courtNo)
|
||||
).toEqual([1]);
|
||||
expect(await db.courtServiceJudges.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('clears service judge duty and quick buttons when disabling the role', async () => {
|
||||
await db.courtServiceJudges.add({ courtNo: 1, umpireId: 1 });
|
||||
await db.waitingServiceJudges.add({ serviceJudgeId: 2, order: 1 });
|
||||
await service.update({
|
||||
withServiceJudge: false,
|
||||
waitingListQuickButtons: true
|
||||
});
|
||||
expect(await db.courtServiceJudges.count()).toBe(0);
|
||||
expect(await db.waitingServiceJudges.count()).toBe(0);
|
||||
expect((await db.settings.get(1))?.waitingListQuickButtons).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -23,24 +23,46 @@ export class SettingsService {
|
||||
* Update settings
|
||||
*/
|
||||
async update(changes: Partial<Omit<Settings, 'id'>>): Promise<void> {
|
||||
const current = await db.settings.get(1);
|
||||
|
||||
if (!current) {
|
||||
if (
|
||||
changes.numberOfCourts !== undefined &&
|
||||
(!Number.isSafeInteger(changes.numberOfCourts) ||
|
||||
changes.numberOfCourts < 1)
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
await db.settings.update(1, {
|
||||
...changes,
|
||||
...(!(changes.withServiceJudge ?? current.withServiceJudge)
|
||||
? { waitingListQuickButtons: false }
|
||||
: {})
|
||||
});
|
||||
await db.transaction(
|
||||
'rw',
|
||||
[
|
||||
db.settings,
|
||||
db.courtUmpires,
|
||||
db.courtServiceJudges,
|
||||
db.waitingServiceJudges
|
||||
],
|
||||
async () => {
|
||||
await this.ensureSettings();
|
||||
const current = (await db.settings.get(1))!;
|
||||
const next = { ...current, ...changes };
|
||||
if (!next.withServiceJudge) next.waitingListQuickButtons = false;
|
||||
await db.courtUmpires
|
||||
.where('courtNo')
|
||||
.above(next.numberOfCourts)
|
||||
.delete();
|
||||
await db.courtServiceJudges
|
||||
.where('courtNo')
|
||||
.above(next.numberOfCourts)
|
||||
.delete();
|
||||
if (!next.withServiceJudge) {
|
||||
await db.courtServiceJudges.clear();
|
||||
await db.waitingServiceJudges.clear();
|
||||
}
|
||||
await db.settings.put(next);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureSettings() {
|
||||
const existing = await db.settings.get(1);
|
||||
|
||||
if (!existing) {
|
||||
await db.transaction('rw', db.settings, async () => {
|
||||
if (await db.settings.get(1)) return;
|
||||
await db.settings.put({
|
||||
id: 1,
|
||||
numberOfCourts: 5,
|
||||
@@ -48,6 +70,6 @@ export class SettingsService {
|
||||
showAlert: true,
|
||||
waitingListQuickButtons: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,31 @@ describe('UmpireService persistence', () => {
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.umpires.clear();
|
||||
await Promise.all([
|
||||
db.umpires.clear(),
|
||||
db.waitingUmpires.clear(),
|
||||
db.waitingServiceJudges.clear(),
|
||||
db.courtUmpires.clear(),
|
||||
db.courtServiceJudges.clear()
|
||||
]);
|
||||
service = TestBed.inject(UmpireService);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await db.umpires.clear();
|
||||
await Promise.all([
|
||||
db.umpires.clear(),
|
||||
db.waitingUmpires.clear(),
|
||||
db.waitingServiceJudges.clear(),
|
||||
db.courtUmpires.clear(),
|
||||
db.courtServiceJudges.clear()
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates and retrieves a complete official record', async () => {
|
||||
const id = await service.create(official);
|
||||
expect(await service.getById(id)).toEqual({ ...official, id });
|
||||
expect('id' in official).toBeFalse();
|
||||
});
|
||||
|
||||
it('updates selected fields while retaining the rest of the record', async () => {
|
||||
@@ -53,6 +66,28 @@ describe('UmpireService persistence', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes dependent duty records and closes the queue gap', async () => {
|
||||
const id = await service.create(official);
|
||||
const retained = await service.create({ ...official, firstName: 'Sam' });
|
||||
await db.waitingUmpires.bulkAdd([
|
||||
{ umpireId: id, order: 1 },
|
||||
{ umpireId: retained, order: 2 }
|
||||
]);
|
||||
await db.waitingServiceJudges.add({ serviceJudgeId: id, order: 1 });
|
||||
await db.courtUmpires.add({ umpireId: id, courtNo: 1 });
|
||||
await db.courtServiceJudges.add({ umpireId: id, courtNo: 2 });
|
||||
await service.delete(id);
|
||||
expect(
|
||||
(await db.waitingUmpires.toArray()).map((item) => [
|
||||
item.umpireId,
|
||||
item.order
|
||||
])
|
||||
).toEqual([[retained, 1]]);
|
||||
expect(await db.waitingServiceJudges.count()).toBe(0);
|
||||
expect(await db.courtUmpires.count()).toBe(0);
|
||||
expect(await db.courtServiceJudges.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('clears all officials', async () => {
|
||||
await service.create(official);
|
||||
await service.create({ ...official, firstName: 'Sam' });
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { db } from 'db';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { from } from 'rxjs';
|
||||
import { Umpire } from '../../../db';
|
||||
import { Umpire } from 'db';
|
||||
import { WaitingUmpiresService } from './waiting-umpires.service';
|
||||
import { WaitingServiceJudgesService } from './waiting-service-judges.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UmpireService {
|
||||
private readonly waitingUmpires = inject(WaitingUmpiresService);
|
||||
private readonly waitingJudges = inject(WaitingServiceJudgesService);
|
||||
/**
|
||||
* Reactive signal with all umpires
|
||||
*/
|
||||
@@ -23,9 +27,7 @@ export class UmpireService {
|
||||
* Create
|
||||
*/
|
||||
async create(umpire: Omit<Umpire, 'id'>): Promise<number> {
|
||||
return await db.umpires.add({
|
||||
...umpire
|
||||
} as Umpire);
|
||||
return db.umpires.add({ ...umpire });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +51,50 @@ export class UmpireService {
|
||||
* Delete
|
||||
*/
|
||||
async delete(id: number): Promise<void> {
|
||||
await db.transaction(
|
||||
'rw',
|
||||
[
|
||||
db.umpires,
|
||||
db.waitingUmpires,
|
||||
db.waitingServiceJudges,
|
||||
db.courtUmpires,
|
||||
db.courtServiceJudges
|
||||
],
|
||||
async () => {
|
||||
await db.waitingUmpires.where('umpireId').equals(id).delete();
|
||||
await db.waitingServiceJudges
|
||||
.where('serviceJudgeId')
|
||||
.equals(id)
|
||||
.delete();
|
||||
await db.courtUmpires.where('umpireId').equals(id).delete();
|
||||
await db.courtServiceJudges.where('umpireId').equals(id).delete();
|
||||
await this.waitingUmpires.reinitOrders();
|
||||
await this.waitingJudges.reinitOrders();
|
||||
await db.umpires.delete(id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
await db.transaction(
|
||||
'rw',
|
||||
[
|
||||
db.umpires,
|
||||
db.waitingUmpires,
|
||||
db.waitingServiceJudges,
|
||||
db.courtUmpires,
|
||||
db.courtServiceJudges
|
||||
],
|
||||
async () => {
|
||||
await db.waitingUmpires.clear();
|
||||
await db.waitingServiceJudges.clear();
|
||||
await db.courtUmpires.clear();
|
||||
await db.courtServiceJudges.clear();
|
||||
await db.umpires.clear();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,31 +22,26 @@ export class WaitingServiceJudgesService {
|
||||
* Add new umpire to queue with max(order) + 1
|
||||
*/
|
||||
async add(serviceJudgeId: number, toPosition?: number): Promise<number> {
|
||||
const id = await db.transaction('rw', db.waitingServiceJudges, async () => {
|
||||
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({
|
||||
const id = await db.waitingServiceJudges.add({
|
||||
serviceJudgeId,
|
||||
order: nextOrder
|
||||
order: last ? last.order + 1 : 1
|
||||
});
|
||||
});
|
||||
|
||||
if (toPosition !== undefined) {
|
||||
if (toPosition !== undefined)
|
||||
await this.moveToPosition(serviceJudgeId, toPosition);
|
||||
}
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove from queue
|
||||
*/
|
||||
async remove(id: number): Promise<void> {
|
||||
await db.transaction('rw', db.waitingServiceJudges, async () => {
|
||||
await db.waitingServiceJudges.delete(id);
|
||||
|
||||
await this.reinitOrders();
|
||||
});
|
||||
}
|
||||
|
||||
async removeByUmpireId(umpireId: number): Promise<void> {
|
||||
@@ -106,7 +101,7 @@ export class WaitingServiceJudgesService {
|
||||
.toArray();
|
||||
|
||||
for (const item of affected) {
|
||||
await db.waitingServiceJudges.update(item.id!, {
|
||||
await db.waitingServiceJudges.update(item.id, {
|
||||
order: item.order + 1
|
||||
});
|
||||
}
|
||||
@@ -123,13 +118,13 @@ export class WaitingServiceJudgesService {
|
||||
.toArray();
|
||||
|
||||
for (const item of affected) {
|
||||
await db.waitingServiceJudges.update(item.id!, {
|
||||
await db.waitingServiceJudges.update(item.id, {
|
||||
order: item.order - 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await db.waitingServiceJudges.update(current.id!, {
|
||||
await db.waitingServiceJudges.update(current.id, {
|
||||
order: newOrder
|
||||
});
|
||||
});
|
||||
@@ -145,7 +140,7 @@ export class WaitingServiceJudgesService {
|
||||
const expectedOrder = i + 1;
|
||||
|
||||
if (item.order !== expectedOrder) {
|
||||
await db.waitingServiceJudges.update(item.id!, {
|
||||
await db.waitingServiceJudges.update(item.id, {
|
||||
order: expectedOrder
|
||||
});
|
||||
}
|
||||
@@ -154,7 +149,7 @@ export class WaitingServiceJudgesService {
|
||||
}
|
||||
|
||||
async getCurrentUmpire(): Promise<Umpire | undefined> {
|
||||
const item = await db.waitingServiceJudges.where('order').equals(1).first();
|
||||
const item = await db.waitingServiceJudges.orderBy('order').first();
|
||||
|
||||
return item ? db.umpires.get(item.serviceJudgeId) : undefined;
|
||||
}
|
||||
|
||||
@@ -22,31 +22,26 @@ export class WaitingUmpiresService {
|
||||
* Add new umpire to queue with max(order) + 1
|
||||
*/
|
||||
async add(umpireId: number, toPosition?: number): Promise<number> {
|
||||
const id = await db.transaction('rw', db.waitingUmpires, async () => {
|
||||
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({
|
||||
const id = await db.waitingUmpires.add({
|
||||
umpireId,
|
||||
order: nextOrder
|
||||
order: last ? last.order + 1 : 1
|
||||
});
|
||||
});
|
||||
|
||||
if (toPosition !== undefined) {
|
||||
if (toPosition !== undefined)
|
||||
await this.moveToPosition(umpireId, toPosition);
|
||||
}
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove from queue
|
||||
*/
|
||||
async remove(id: number): Promise<void> {
|
||||
await db.transaction('rw', db.waitingUmpires, async () => {
|
||||
await db.waitingUmpires.delete(id);
|
||||
|
||||
await this.reinitOrders();
|
||||
});
|
||||
}
|
||||
|
||||
async removeByUmpireId(umpireId: number): Promise<void> {
|
||||
@@ -106,7 +101,7 @@ export class WaitingUmpiresService {
|
||||
.toArray();
|
||||
|
||||
for (const item of affected) {
|
||||
await db.waitingUmpires.update(item.id!, {
|
||||
await db.waitingUmpires.update(item.id, {
|
||||
order: item.order + 1
|
||||
});
|
||||
}
|
||||
@@ -123,13 +118,13 @@ export class WaitingUmpiresService {
|
||||
.toArray();
|
||||
|
||||
for (const item of affected) {
|
||||
await db.waitingUmpires.update(item.id!, {
|
||||
await db.waitingUmpires.update(item.id, {
|
||||
order: item.order - 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await db.waitingUmpires.update(current.id!, {
|
||||
await db.waitingUmpires.update(current.id, {
|
||||
order: newOrder
|
||||
});
|
||||
});
|
||||
@@ -145,7 +140,7 @@ export class WaitingUmpiresService {
|
||||
const expectedOrder = i + 1;
|
||||
|
||||
if (item.order !== expectedOrder) {
|
||||
await db.waitingUmpires.update(item.id!, {
|
||||
await db.waitingUmpires.update(item.id, {
|
||||
order: expectedOrder
|
||||
});
|
||||
}
|
||||
@@ -154,7 +149,7 @@ export class WaitingUmpiresService {
|
||||
}
|
||||
|
||||
async getCurrentUmpire(): Promise<Umpire | undefined> {
|
||||
const item = await db.waitingUmpires.where('order').equals(1).first();
|
||||
const item = await db.waitingUmpires.orderBy('order').first();
|
||||
|
||||
return item ? db.umpires.get(item.umpireId) : undefined;
|
||||
}
|
||||
|
||||
+39
-39
@@ -11,16 +11,16 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-grid [fixed]="'fixed'" class="bottom-lists">
|
||||
<ion-grid [fixed]="true" class="bottom-lists">
|
||||
<ion-row class="court-headers">
|
||||
<ion-col [size]="1" class="ion-display-none ion-display-xl-block">
|
||||
<ion-item [lines]="'none'">{{ 'court' | translate }}</ion-item>
|
||||
<ion-col size="1" class="ion-display-none ion-display-xl-block">
|
||||
<ion-item lines="none">{{ 'court' | translate }}</ion-item>
|
||||
</ion-col>
|
||||
<ion-col [size]="1" class="ion-display-xl-none">
|
||||
<ion-item [lines]="'none'">{{ 'shortCourtName' | translate }}</ion-item>
|
||||
<ion-col size="1" class="ion-display-xl-none">
|
||||
<ion-item lines="none">{{ 'shortCourtName' | translate }}</ion-item>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<ion-item [lines]="'none'">
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-center">
|
||||
{{ 'umpire' | translate }}
|
||||
</ion-label>
|
||||
@@ -28,7 +28,7 @@
|
||||
</ion-col>
|
||||
@if (serviceJudgeEnabled()) {
|
||||
<ion-col>
|
||||
<ion-item [lines]="'none'">
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-center">
|
||||
{{ 'serviceJudge' | translate }}
|
||||
</ion-label>
|
||||
@@ -37,24 +37,22 @@
|
||||
}
|
||||
<ion-col size="2" sizeLg="1"></ion-col>
|
||||
</ion-row>
|
||||
@for (item of
|
||||
[].constructor($safeNavigationMigration(settings()?.numberOfCourts)); track
|
||||
$index) {
|
||||
@for (courtNo of courtIndexes(); track courtNo) {
|
||||
<ion-row>
|
||||
<ion-col [size]="1" class="court-number">
|
||||
<ion-col size="1" class="court-number">
|
||||
<ion-list>
|
||||
<ion-item [lines]="'none'"> {{ $index + 1 }}. </ion-item>
|
||||
<ion-item lines="none"> {{ courtNo }}. </ion-item>
|
||||
</ion-list>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<ion-list
|
||||
[lines]="'none'"
|
||||
lines="none"
|
||||
cdkDropList
|
||||
[id]="`court-umpire-${$index + 1}`"
|
||||
[cdkDropListData]="umpireByCourt().get($index + 1) ? [umpireByCourt().get($index + 1)] : []"
|
||||
[id]="`court-umpire-${courtNo}`"
|
||||
[cdkDropListData]="umpireByCourt().get(courtNo) ? [umpireByCourt().get(courtNo)] : []"
|
||||
[cdkDropListEnterPredicate]="canDropUmpire"
|
||||
(cdkDropListDropped)="dropToUmpire($event, $index + 1)">
|
||||
@if (umpireByCourt().get($index + 1); as umpire) {
|
||||
(cdkDropListDropped)="dropToUmpire($event, courtNo)">
|
||||
@if (umpireByCourt().get(courtNo); as umpire) {
|
||||
|
||||
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||
@@ -69,12 +67,13 @@
|
||||
@if (serviceJudgeEnabled()) {
|
||||
<ion-col>
|
||||
<ion-list
|
||||
[lines]="'none'"
|
||||
lines="none"
|
||||
cdkDropList
|
||||
[id]="`court-service-judge-${$index + 1}`"
|
||||
[cdkDropListData]="serviceJudgeByCourt().get($index + 1) ? [serviceJudgeByCourt().get($index + 1)] : []"
|
||||
(cdkDropListDropped)="dropToServiceJudge($event, $index + 1)">
|
||||
@if (serviceJudgeByCourt().get($index + 1); as umpire) {
|
||||
[id]="`court-service-judge-${courtNo}`"
|
||||
[cdkDropListData]="serviceJudgeByCourt().get(courtNo) ? [serviceJudgeByCourt().get(courtNo)] : []"
|
||||
[cdkDropListEnterPredicate]="canDropUmpire"
|
||||
(cdkDropListDropped)="dropToServiceJudge($event, courtNo)">
|
||||
@if (serviceJudgeByCourt().get(courtNo); as umpire) {
|
||||
|
||||
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||
@@ -90,25 +89,26 @@
|
||||
<ion-col size="2" sizeLg="1" class="court-actions">
|
||||
<ion-list>
|
||||
<ion-item lines="none">
|
||||
@if ( (umpireByCourt().get($index + 1) &&
|
||||
serviceJudgeByCourt().get($index + 1)) ||
|
||||
(umpireByCourt().get($index + 1) && !serviceJudgeEnabled()) ) {
|
||||
@if ( (umpireByCourt().get(courtNo) &&
|
||||
serviceJudgeByCourt().get(courtNo)) || (umpireByCourt().get(courtNo)
|
||||
&& !serviceJudgeEnabled()) ) {
|
||||
<ion-button
|
||||
[color]="'danger'"
|
||||
(click)="showRemoveConfirmation($index + 1)">
|
||||
color="danger"
|
||||
[attr.aria-label]="('removeUmpireFromCourt' | translate) + ': ' + courtNo"
|
||||
(click)="showRemoveConfirmation(courtNo)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="large"
|
||||
name="exit-outline"></ion-icon>
|
||||
</ion-button>
|
||||
} @else if ((!umpireByCourt().get($index + 1) &&
|
||||
!serviceJudgeByCourt().get($index + 1) && waitingUmpires().length &&
|
||||
waitingServiceJudges().length) || (!umpireByCourt().get($index + 1)
|
||||
&& waitingUmpires().length && !serviceJudgeEnabled())) {
|
||||
} @else if ((!umpireByCourt().get(courtNo) &&
|
||||
!serviceJudgeByCourt().get(courtNo) && waitingUmpires().length &&
|
||||
waitingServiceJudges().length) || (!umpireByCourt().get(courtNo) &&
|
||||
waitingUmpires().length && !serviceJudgeEnabled())) {
|
||||
<ion-button
|
||||
[color]="'success'"
|
||||
[disabled]=""
|
||||
(click)="showAddConfirmation($index + 1)">
|
||||
color="success"
|
||||
[attr.aria-label]="('sendUmpireToCourt' | translate) + ': ' + courtNo"
|
||||
(click)="showAddConfirmation(courtNo)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="large"
|
||||
@@ -121,7 +121,7 @@
|
||||
</ion-row>
|
||||
}
|
||||
</ion-grid>
|
||||
<ion-grid [fixed]="'fixed'" class="ion-margin-top bottom-lists">
|
||||
<ion-grid [fixed]="true" class="ion-margin-top bottom-lists">
|
||||
<ion-row>
|
||||
<ion-col [size]="!serviceJudgeEnabled() ? 6 : 4">
|
||||
<ion-item color="primary">
|
||||
@@ -131,7 +131,7 @@
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
@if (serviceJudgeEnabled()) {
|
||||
<ion-col [size]="4">
|
||||
<ion-col size="4">
|
||||
<ion-item color="primary">
|
||||
<ion-label class="ion-text-center">
|
||||
{{ 'serviceJudges' | translate }}
|
||||
@@ -150,7 +150,7 @@
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-list
|
||||
[lines]="'none'"
|
||||
lines="none"
|
||||
cdkDropList
|
||||
id="list-waiting-umpires"
|
||||
[cdkDropListData]="waitingUmpires()"
|
||||
@@ -180,7 +180,7 @@
|
||||
@if (serviceJudgeEnabled()) {
|
||||
<ion-col>
|
||||
<ion-list
|
||||
[lines]="'none'"
|
||||
lines="none"
|
||||
cdkDropList
|
||||
id="list-waiting-service-judges"
|
||||
[cdkDropListData]="waitingServiceJudges()"
|
||||
@@ -210,7 +210,7 @@
|
||||
}
|
||||
<ion-col>
|
||||
<ion-list
|
||||
[lines]="'none'"
|
||||
lines="none"
|
||||
cdkDropList
|
||||
cdkDropListSortingDisabled
|
||||
id="list-on-rest"
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { CourtRotationService } from '../services/court-rotation.service';
|
||||
import { Tab1Page } from './tab1.page';
|
||||
import { SettingsService } from '../services/settings-service';
|
||||
import { UmpireService } from '../services/umpire.service';
|
||||
@@ -37,6 +38,7 @@ describe('Tab1Page court rotation', () => {
|
||||
let umpireCourt: jasmine.SpyObj<CourtUmpireService>;
|
||||
let judgeCourt: jasmine.SpyObj<CourtServiceJudgeService>;
|
||||
let duty: jasmine.SpyObj<WaitingDutyService>;
|
||||
let rotation: jasmine.SpyObj<CourtRotationService>;
|
||||
let alerts: jasmine.SpyObj<AlertController>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -78,6 +80,15 @@ describe('Tab1Page court rotation', () => {
|
||||
duty = jasmine.createSpyObj('WaitingDutyService', ['move', 'moveToRest']);
|
||||
duty.move.and.resolveTo();
|
||||
duty.moveToRest.and.resolveTo();
|
||||
rotation = jasmine.createSpyObj('CourtRotationService', [
|
||||
'assignNext',
|
||||
'rotate',
|
||||
'moveToQueue',
|
||||
'moveToCourt',
|
||||
'moveToRest'
|
||||
]);
|
||||
rotation.assignNext.and.resolveTo();
|
||||
rotation.rotate.and.resolveTo();
|
||||
alerts = jasmine.createSpyObj('AlertController', ['create']);
|
||||
waitingUmpire.getCurrentUmpire.and.resolveTo(official(1));
|
||||
waitingJudge.getCurrentUmpire.and.resolveTo(official(2));
|
||||
@@ -91,6 +102,7 @@ describe('Tab1Page court rotation', () => {
|
||||
judgeCourt.removeByUmpireId.and.resolveTo();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: CourtRotationService, useValue: rotation },
|
||||
{ provide: WaitingDutyService, useValue: duty },
|
||||
{ provide: SettingsService, useValue: { settings } },
|
||||
{ provide: UmpireService, useValue: { umpires } },
|
||||
@@ -179,7 +191,7 @@ describe('Tab1Page court rotation', () => {
|
||||
alerts.create.and.resolveTo({ present } as unknown as HTMLIonAlertElement);
|
||||
await component.showAddConfirmation(2);
|
||||
expect(present).toHaveBeenCalledTimes(1);
|
||||
expect(umpireCourt.save).not.toHaveBeenCalled();
|
||||
expect(rotation.assignNext).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
const options = alerts.create.calls.mostRecent().args[0];
|
||||
const confirm = options?.buttons?.find(
|
||||
@@ -190,14 +202,7 @@ describe('Tab1Page court rotation', () => {
|
||||
confirm.handler(undefined);
|
||||
}
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 2,
|
||||
umpireId: 1
|
||||
});
|
||||
expect(judgeCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 2,
|
||||
umpireId: 2
|
||||
});
|
||||
expect(rotation.assignNext).toHaveBeenCalledOnceWith(2, 1, 2);
|
||||
});
|
||||
|
||||
it('returns a lone umpire to the umpire queue after confirming rotation', async () => {
|
||||
@@ -210,7 +215,7 @@ describe('Tab1Page court rotation', () => {
|
||||
const present = jasmine.createSpy('present').and.resolveTo();
|
||||
alerts.create.and.resolveTo({ present } as unknown as HTMLIonAlertElement);
|
||||
await component.showRemoveConfirmation(2);
|
||||
expect(umpireCourt.removeByUmpireId).not.toHaveBeenCalled();
|
||||
expect(rotation.rotate).not.toHaveBeenCalled();
|
||||
const options = alerts.create.calls.mostRecent().args[0];
|
||||
const confirm = options?.buttons?.find(
|
||||
(button) => typeof button !== 'string' && button.role === 'confirm'
|
||||
@@ -220,32 +225,20 @@ describe('Tab1Page court rotation', () => {
|
||||
confirm.handler(undefined);
|
||||
}
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.removeByUmpireId).toHaveBeenCalledOnceWith(3);
|
||||
expect(waitingUmpire.add).toHaveBeenCalledOnceWith(3);
|
||||
expect(waitingJudge.add).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.removeByUmpireId).not.toHaveBeenCalled();
|
||||
expect(rotation.rotate).toHaveBeenCalledOnceWith(2, 3, undefined);
|
||||
});
|
||||
|
||||
it('assigns both queue heads and removes them from their queues', async () => {
|
||||
await component.showAddConfirmation(3);
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 3,
|
||||
umpireId: 1
|
||||
});
|
||||
expect(judgeCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 3,
|
||||
umpireId: 2
|
||||
});
|
||||
expect(waitingUmpire.removeByUmpireId).toHaveBeenCalledOnceWith(1);
|
||||
expect(waitingJudge.removeByUmpireId).toHaveBeenCalledOnceWith(2);
|
||||
expect(rotation.assignNext).toHaveBeenCalledOnceWith(3, 1, 2);
|
||||
expect(alerts.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not assign either role when the required service judge is missing', async () => {
|
||||
waitingJudge.getCurrentUmpire.and.resolveTo(undefined);
|
||||
await component.showAddConfirmation(1);
|
||||
expect(umpireCourt.save).not.toHaveBeenCalled();
|
||||
expect(rotation.assignNext).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
expect(waitingUmpire.removeByUmpireId).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -253,7 +246,7 @@ describe('Tab1Page court rotation', () => {
|
||||
it('does not assign when the umpire queue is empty', async () => {
|
||||
waitingUmpire.getCurrentUmpire.and.resolveTo(undefined);
|
||||
await component.showAddConfirmation(1);
|
||||
expect(umpireCourt.save).not.toHaveBeenCalled();
|
||||
expect(rotation.assignNext).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -261,13 +254,8 @@ describe('Tab1Page court rotation', () => {
|
||||
settings.update((value) => ({ ...value, withServiceJudge: false }));
|
||||
await component.showAddConfirmation(2);
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 2,
|
||||
umpireId: 1
|
||||
});
|
||||
expect(waitingUmpire.removeByUmpireId).toHaveBeenCalledOnceWith(1);
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
expect(waitingJudge.removeByUmpireId).not.toHaveBeenCalled();
|
||||
expect(rotation.assignNext).toHaveBeenCalledOnceWith(2, 1, undefined);
|
||||
expect(waitingJudge.getCurrentUmpire).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('swaps outgoing officials into the opposite role queues', async () => {
|
||||
@@ -275,19 +263,23 @@ describe('Tab1Page court rotation', () => {
|
||||
courtJudges.set([{ id: 1, umpireId: 4, courtNo: 2 }]);
|
||||
await component.showRemoveConfirmation(2);
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.removeByUmpireId).toHaveBeenCalledOnceWith(3);
|
||||
expect(judgeCourt.removeByUmpireId).toHaveBeenCalledOnceWith(4);
|
||||
expect(waitingUmpire.add).toHaveBeenCalledOnceWith(4);
|
||||
expect(waitingJudge.add).toHaveBeenCalledOnceWith(3);
|
||||
expect(rotation.rotate).toHaveBeenCalledOnceWith(2, 3, 4);
|
||||
});
|
||||
|
||||
it('leaves an incomplete court unchanged when service judges are required', async () => {
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
await component.showRemoveConfirmation(2);
|
||||
expect(umpireCourt.removeByUmpireId).not.toHaveBeenCalled();
|
||||
expect(rotation.rotate).not.toHaveBeenCalled();
|
||||
expect(waitingUmpire.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rotates a lone umpire without confirmation when service judges are disabled', async () => {
|
||||
settings.update((value) => ({ ...value, withServiceJudge: false }));
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
await component.showRemoveConfirmation(2);
|
||||
expect(rotation.rotate).toHaveBeenCalledOnceWith(2, 3, undefined);
|
||||
});
|
||||
|
||||
it('derives rest from all four duty states and reacts to assignment changes', () => {
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
courtJudges.set([{ id: 1, umpireId: 4, courtNo: 2 }]);
|
||||
|
||||
+72
-175
@@ -19,6 +19,7 @@ import {
|
||||
IonButton,
|
||||
AlertController
|
||||
} from '@ionic/angular';
|
||||
import { CourtRotationService } from '../services/court-rotation.service';
|
||||
import { SettingsService } from '../services/settings-service';
|
||||
import { UmpireService } from '../services/umpire.service';
|
||||
import { Umpire } from 'db';
|
||||
@@ -82,8 +83,10 @@ export class Tab1Page {
|
||||
readonly courtServiceJudgeService = inject(CourtServiceJudgeService);
|
||||
readonly translationService = inject(TranslationService);
|
||||
|
||||
private alertController = inject(AlertController);
|
||||
private fullnamePipe = inject(FullnamePipe);
|
||||
readonly rotationService = inject(CourtRotationService);
|
||||
|
||||
private readonly alertController = inject(AlertController);
|
||||
private readonly fullnamePipe = inject(FullnamePipe);
|
||||
|
||||
/**
|
||||
* Raw signals from services
|
||||
@@ -313,125 +316,76 @@ export class Tab1Page {
|
||||
{
|
||||
text: this.translationService.translate('yes'),
|
||||
role: 'confirm',
|
||||
handler: () => {
|
||||
void move();
|
||||
}
|
||||
handler: () => 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;
|
||||
async dropToRest(
|
||||
event: CdkDragDrop<Umpire[], Umpire[], Umpire>
|
||||
): Promise<void> {
|
||||
if (event.previousContainer === event.container) return;
|
||||
const source = event.previousContainer.id;
|
||||
if (
|
||||
comingFrom === 'list-waiting-umpires' ||
|
||||
comingFrom === 'list-waiting-service-judges'
|
||||
source === 'list-waiting-umpires' ||
|
||||
source === 'list-waiting-service-judges'
|
||||
) {
|
||||
await this.waitingDutyService.moveToRest(umpireToMove.id);
|
||||
return;
|
||||
}
|
||||
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||
}
|
||||
}
|
||||
|
||||
dropToWaitingServiceJudge(event: CdkDragDrop<Umpire[]>) {
|
||||
const umpireToMove = event.item.data;
|
||||
if (event.previousContainer === event.container) {
|
||||
this.waitingServiceJudgeService.moveToPosition(
|
||||
umpireToMove.id,
|
||||
event.currentIndex + 1
|
||||
);
|
||||
return;
|
||||
}
|
||||
const comingFrom = event.previousContainer.id;
|
||||
|
||||
this.waitingServiceJudgeService.add(
|
||||
umpireToMove.id,
|
||||
event.currentIndex + 1
|
||||
);
|
||||
|
||||
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||
}
|
||||
|
||||
dropToWaitingUmpire(event: CdkDragDrop<Umpire[]>) {
|
||||
const umpireToMove = event.item.data;
|
||||
if (event.previousContainer === event.container) {
|
||||
this.waitingUmpireService.moveToPosition(
|
||||
umpireToMove.id,
|
||||
event.currentIndex + 1
|
||||
);
|
||||
return;
|
||||
await this.waitingDutyService.moveToRest(event.item.data.id);
|
||||
} else {
|
||||
const comingFrom = event.previousContainer.id;
|
||||
this.waitingUmpireService.add(umpireToMove.id, event.currentIndex + 1);
|
||||
|
||||
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||
await this.rotationService.moveToRest(event.item.data.id);
|
||||
}
|
||||
}
|
||||
|
||||
dropToUmpire(event: CdkDragDrop<(Umpire | undefined)[]>, courtNo: number) {
|
||||
const targetUmpires = event.container.data;
|
||||
|
||||
if (targetUmpires.length > 0) {
|
||||
return;
|
||||
async dropToWaitingServiceJudge(
|
||||
event: CdkDragDrop<Umpire[], Umpire[], Umpire>
|
||||
): Promise<void> {
|
||||
await this.rotationService.moveToQueue(
|
||||
event.item.data.id,
|
||||
'serviceJudge',
|
||||
event.currentIndex + 1
|
||||
);
|
||||
}
|
||||
|
||||
const comingFrom = event.previousContainer.id;
|
||||
const umpireToMove = event.item.data;
|
||||
this.courtUmpireService.save({ umpireId: umpireToMove.id, courtNo });
|
||||
|
||||
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||
async dropToWaitingUmpire(
|
||||
event: CdkDragDrop<Umpire[], Umpire[], Umpire>
|
||||
): Promise<void> {
|
||||
await this.rotationService.moveToQueue(
|
||||
event.item.data.id,
|
||||
'umpire',
|
||||
event.currentIndex + 1
|
||||
);
|
||||
}
|
||||
|
||||
dropToServiceJudge(
|
||||
event: CdkDragDrop<(Umpire | undefined)[]>,
|
||||
async dropToUmpire(
|
||||
event: CdkDragDrop<(Umpire | undefined)[], unknown, Umpire>,
|
||||
courtNo: number
|
||||
) {
|
||||
const targetUmpires = event.container.data;
|
||||
|
||||
if (targetUmpires.length > 0) {
|
||||
return;
|
||||
): Promise<void> {
|
||||
if (event.container.data.length) return;
|
||||
await this.rotationService.moveToCourt(
|
||||
event.item.data.id,
|
||||
'umpire',
|
||||
courtNo
|
||||
);
|
||||
}
|
||||
|
||||
const comingFrom = event.previousContainer.id;
|
||||
const umpireToMove = event.item.data;
|
||||
this.courtServiceJudgeService.save({ umpireId: umpireToMove.id, courtNo });
|
||||
|
||||
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||
async dropToServiceJudge(
|
||||
event: CdkDragDrop<(Umpire | undefined)[], unknown, Umpire>,
|
||||
courtNo: number
|
||||
): Promise<void> {
|
||||
if (event.container.data.length) return;
|
||||
await this.rotationService.moveToCourt(
|
||||
event.item.data.id,
|
||||
'serviceJudge',
|
||||
courtNo
|
||||
);
|
||||
}
|
||||
|
||||
private removeFromOriginalPlace(
|
||||
umpireToMove: Umpire,
|
||||
comingFrom: string
|
||||
): void {
|
||||
if ('list-waiting-service-judges' === comingFrom) {
|
||||
this.waitingServiceJudgeService.removeByUmpireId(umpireToMove.id);
|
||||
}
|
||||
|
||||
if ('list-waiting-umpires' === comingFrom) {
|
||||
this.waitingUmpireService.removeByUmpireId(umpireToMove.id);
|
||||
}
|
||||
|
||||
if (comingFrom.startsWith('court-umpire')) {
|
||||
this.courtUmpireService.removeByUmpireId(umpireToMove.id);
|
||||
}
|
||||
|
||||
if (comingFrom.startsWith('court-service-judge')) {
|
||||
this.courtServiceJudgeService.removeByUmpireId(umpireToMove.id);
|
||||
}
|
||||
}
|
||||
|
||||
canDropUmpire = (
|
||||
drag: CdkDrag<Umpire>,
|
||||
drop: CdkDropList<Umpire[]>
|
||||
): boolean => {
|
||||
return drop.data.length === 0;
|
||||
};
|
||||
readonly canDropUmpire = (
|
||||
_drag: CdkDrag<Umpire>,
|
||||
drop: CdkDropList<(Umpire | undefined)[]>
|
||||
): boolean => drop.data.length === 0;
|
||||
|
||||
public async showRemoveConfirmation(courtNo: number) {
|
||||
const umpire = this.umpireByCourt().get(courtNo);
|
||||
@@ -445,7 +399,7 @@ export class Tab1Page {
|
||||
}
|
||||
|
||||
if (!this.settings()?.showAlert) {
|
||||
this.removeUmpiresFromCourt(courtNo);
|
||||
await this.rotationService.rotate(courtNo, umpire.id, serviceJudge?.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -472,14 +426,8 @@ export class Tab1Page {
|
||||
{
|
||||
text: this.translationService.translate('yes'),
|
||||
role: 'confirm',
|
||||
handler: () => {
|
||||
if (this.serviceJudgeEnabled()) {
|
||||
this.removeUmpiresFromCourt(courtNo);
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeUmpireFromCourt(courtNo);
|
||||
}
|
||||
handler: () =>
|
||||
this.rotationService.rotate(courtNo, umpire.id, serviceJudge?.id)
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -487,41 +435,11 @@ export class Tab1Page {
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for removing both umpire and service judge from the court (when service judge is enabled).
|
||||
*/
|
||||
private async removeUmpiresFromCourt(courtNo: number) {
|
||||
const umpire = this.umpireByCourt().get(courtNo);
|
||||
const serviceJudge = this.serviceJudgeByCourt().get(courtNo);
|
||||
|
||||
if (typeof umpire === 'undefined' || typeof serviceJudge === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.courtUmpireService.removeByUmpireId(umpire.id);
|
||||
await this.courtServiceJudgeService.removeByUmpireId(serviceJudge.id);
|
||||
await this.waitingUmpireService.add(serviceJudge.id);
|
||||
await this.waitingServiceJudgeService.add(umpire.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for removing 1 umpire only (when service judge is not enabled).
|
||||
*/
|
||||
private async removeUmpireFromCourt(courtNo: number) {
|
||||
const umpire = this.umpireByCourt().get(courtNo);
|
||||
|
||||
if (typeof umpire === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.courtUmpireService.removeByUmpireId(umpire.id);
|
||||
await this.waitingUmpireService.add(umpire.id);
|
||||
}
|
||||
|
||||
public async showAddConfirmation(courtNo: number) {
|
||||
const umpire = await this.waitingUmpireService.getCurrentUmpire();
|
||||
const serviceJudge =
|
||||
await this.waitingServiceJudgeService.getCurrentUmpire();
|
||||
const serviceJudge = this.serviceJudgeEnabled()
|
||||
? await this.waitingServiceJudgeService.getCurrentUmpire()
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
typeof umpire === 'undefined' ||
|
||||
@@ -531,16 +449,16 @@ export class Tab1Page {
|
||||
}
|
||||
|
||||
if (!this.settings()?.showAlert) {
|
||||
if (this.serviceJudgeEnabled() && serviceJudge) {
|
||||
this.addUmpiresToCourt(courtNo, umpire, serviceJudge);
|
||||
return;
|
||||
}
|
||||
this.addUmpireToCourt(courtNo, umpire);
|
||||
await this.rotationService.assignNext(
|
||||
courtNo,
|
||||
umpire.id,
|
||||
serviceJudge?.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const alert = await this.alertController.create({
|
||||
header: this.translationService.translate('court', {
|
||||
header: this.translationService.translate('courtConfirm', {
|
||||
courtNo
|
||||
}),
|
||||
|
||||
@@ -566,37 +484,16 @@ export class Tab1Page {
|
||||
{
|
||||
text: this.translationService.translate('yes'),
|
||||
role: 'confirm',
|
||||
handler: () => {
|
||||
if (this.serviceJudgeEnabled() && serviceJudge) {
|
||||
this.addUmpiresToCourt(courtNo, umpire, serviceJudge);
|
||||
} else {
|
||||
this.addUmpireToCourt(courtNo, umpire);
|
||||
}
|
||||
}
|
||||
handler: () =>
|
||||
this.rotationService.assignNext(
|
||||
courtNo,
|
||||
umpire.id,
|
||||
serviceJudge?.id
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
private async addUmpiresToCourt(
|
||||
courtNo: number,
|
||||
umpire: Umpire,
|
||||
serviceJudge: Umpire
|
||||
) {
|
||||
await this.courtUmpireService.save({ courtNo, umpireId: umpire.id });
|
||||
await this.courtServiceJudgeService.save({
|
||||
courtNo,
|
||||
umpireId: serviceJudge.id
|
||||
});
|
||||
|
||||
await this.waitingUmpireService.removeByUmpireId(umpire.id);
|
||||
await this.waitingServiceJudgeService.removeByUmpireId(serviceJudge.id);
|
||||
}
|
||||
|
||||
private async addUmpireToCourt(courtNo: number, umpire: Umpire) {
|
||||
await this.courtUmpireService.save({ courtNo, umpireId: umpire.id });
|
||||
await this.waitingUmpireService.removeByUmpireId(umpire.id);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<ion-header [translucent]="true">
|
||||
<ion-toolbar>
|
||||
<ion-title> {{ 'courtOfficials' | translate }} </ion-title>
|
||||
<ion-buttons [slot]="'end'">
|
||||
<ion-button [color]="'primary'" [fill]="'solid'" id="open-modal">
|
||||
<ion-buttons slot="end">
|
||||
<ion-button color="primary" fill="solid" (click)="modal.present()">
|
||||
<ion-icon slot="start" name="add"></ion-icon>
|
||||
{{ 'add' | translate }}
|
||||
</ion-button>
|
||||
@@ -13,7 +13,7 @@
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-header collapse="condense">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">Játékvezetők</ion-title>
|
||||
<ion-title size="large">{{ 'courtOfficials' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
<ion-item>
|
||||
<ion-label> {{ umpire | fullname }} </ion-label>
|
||||
<ion-button
|
||||
[color]="'danger'"
|
||||
color="danger"
|
||||
slot="end"
|
||||
[attr.aria-label]="('confirmDeleteUmpire' | translate) + ': ' + (umpire | fullname)"
|
||||
(click)="showDeleteConfirmation(umpire)">
|
||||
<ion-icon slot="icon-only" name="trash"></ion-icon>
|
||||
</ion-button>
|
||||
@@ -39,19 +40,19 @@
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<ion-modal trigger="open-modal" (willDismiss)="onWillDismiss($event)">
|
||||
<ion-modal [canDismiss]="!saving">
|
||||
<ng-template>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="cancel()"
|
||||
<ion-button [disabled]="saving" (click)="cancel()"
|
||||
>{{ 'cancel' | translate }}</ion-button
|
||||
>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'newCourtOfficial' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button
|
||||
[disabled]="!isValidUmpire()"
|
||||
[disabled]="saving || !isValidUmpire()"
|
||||
(click)="confirm()"
|
||||
[strong]="true">
|
||||
{{ 'save' | translate }}
|
||||
|
||||
@@ -1,18 +1,78 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { signal } from '@angular/core';
|
||||
import { UmpireService } from '../services/umpire.service';
|
||||
import { Tab2Page } from './tab2.page';
|
||||
|
||||
describe('Tab2Page', () => {
|
||||
let component: Tab2Page;
|
||||
let fixture: ComponentFixture<Tab2Page>;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: UmpireService,
|
||||
useValue: { umpires: signal([]), create: async () => 1 }
|
||||
}
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(Tab2Page);
|
||||
component = fixture.componentInstance;
|
||||
document.body.appendChild(fixture.nativeElement);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => fixture.nativeElement.remove());
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('rejects whitespace-only names', async () => {
|
||||
component.lastName = ' ';
|
||||
component.firstName = 'Alex';
|
||||
expect(component.isValidUmpire()).toBeFalse();
|
||||
const dismiss = spyOn(component.modal, 'dismiss');
|
||||
await component.confirm();
|
||||
expect(dismiss).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the form open and preserves the draft when saving fails', async () => {
|
||||
component.lastName = 'Smith';
|
||||
component.firstName = 'Alex';
|
||||
spyOn(component.umpireService, 'create').and.callFake(
|
||||
() =>
|
||||
new Promise<number>((_resolve, reject) => {
|
||||
setTimeout(() => reject(new Error('write failed')), 0);
|
||||
})
|
||||
);
|
||||
const present = jasmine.createSpy('present').and.resolveTo();
|
||||
spyOn(component.alertController, 'create').and.resolveTo({
|
||||
present
|
||||
} as unknown as HTMLIonAlertElement);
|
||||
spyOn(console, 'error');
|
||||
const dismiss = spyOn(component.modal, 'dismiss');
|
||||
await component.confirm();
|
||||
expect(present).toHaveBeenCalled();
|
||||
expect(dismiss).not.toHaveBeenCalled();
|
||||
expect(component.firstName).toBe('Alex');
|
||||
expect(component.saving).toBeFalse();
|
||||
});
|
||||
|
||||
it('persists trimmed names before closing and prevents repeated saves', async () => {
|
||||
component.lastName = ' Smith ';
|
||||
component.firstName = ' Alex ';
|
||||
component.country = ' GB ';
|
||||
const create = spyOn(component.umpireService, 'create').and.resolveTo(1);
|
||||
const dismiss = spyOn(component.modal, 'dismiss').and.resolveTo(true);
|
||||
await Promise.all([component.confirm(), component.confirm()]);
|
||||
expect(create).toHaveBeenCalledOnceWith({
|
||||
firstName: 'Alex',
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: ''
|
||||
});
|
||||
expect(dismiss).toHaveBeenCalledOnceWith(null, 'confirm');
|
||||
expect(component.firstName).toBe('');
|
||||
});
|
||||
});
|
||||
+27
-37
@@ -28,7 +28,6 @@ import { UmpireService } from '../services/umpire.service';
|
||||
import { Umpire } from 'db';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { add, trash } from 'ionicons/icons';
|
||||
import { OverlayEventDetail } from '@ionic/core/components';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FullnamePipe } from '../fullname-pipe';
|
||||
import { TranslatePipe } from '../i18n/translation.pipe';
|
||||
@@ -75,12 +74,14 @@ export class Tab2Page {
|
||||
|
||||
public readonly umpires = this.umpireService.umpires;
|
||||
|
||||
saving = false;
|
||||
|
||||
public lastName: string = '';
|
||||
public firstName: string = '';
|
||||
public country: string = '';
|
||||
public gender: string = '';
|
||||
|
||||
public genders: { label: TranslationKey; code: string }[] = [
|
||||
public readonly genders: { label: TranslationKey; code: string }[] = [
|
||||
{ label: 'male', code: 'M' },
|
||||
{ label: 'female', code: 'W' }
|
||||
];
|
||||
@@ -89,36 +90,32 @@ export class Tab2Page {
|
||||
addIcons({ add, trash });
|
||||
}
|
||||
|
||||
public async onWillDismiss(event: CustomEvent<OverlayEventDetail>) {
|
||||
if (event.detail.role !== 'confirm') {
|
||||
return;
|
||||
cancel(): Promise<boolean> {
|
||||
return this.modal.dismiss(null, 'cancel');
|
||||
}
|
||||
|
||||
const newUmpire: Umpire = event.detail.data;
|
||||
|
||||
async confirm(): Promise<void> {
|
||||
if (this.saving || !this.isValidUmpire()) return;
|
||||
this.saving = true;
|
||||
try {
|
||||
await this.umpireService.create({
|
||||
firstName: newUmpire.firstName,
|
||||
lastName: newUmpire.lastName,
|
||||
country: newUmpire.country,
|
||||
gender: newUmpire.gender
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.modal.dismiss(null, 'cancel');
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.modal.dismiss(
|
||||
{
|
||||
lastName: this.lastName,
|
||||
firstName: this.firstName,
|
||||
country: this.country,
|
||||
lastName: this.lastName.trim(),
|
||||
firstName: this.firstName.trim(),
|
||||
country: this.country.trim(),
|
||||
gender: this.gender
|
||||
},
|
||||
'confirm'
|
||||
);
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save court official:', error);
|
||||
const alert = await this.alertController.create({
|
||||
header: this.translationService.translate('saveFailed'),
|
||||
buttons: ['OK']
|
||||
});
|
||||
await alert.present();
|
||||
return;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
await this.modal.dismiss(null, 'confirm');
|
||||
this.lastName = '';
|
||||
this.firstName = '';
|
||||
this.country = '';
|
||||
@@ -126,7 +123,7 @@ export class Tab2Page {
|
||||
}
|
||||
|
||||
isValidUmpire(): boolean {
|
||||
return this.lastName !== '' && this.firstName !== '';
|
||||
return this.lastName.trim().length > 0 && this.firstName.trim().length > 0;
|
||||
}
|
||||
|
||||
public async showDeleteConfirmation(umpire: Umpire): Promise<void> {
|
||||
@@ -142,18 +139,11 @@ export class Tab2Page {
|
||||
{
|
||||
text: this.translationService.translate('yes'),
|
||||
role: 'confirm',
|
||||
handler: () => {
|
||||
// TODO: also remove from first tab
|
||||
this.removeUmpire(umpire);
|
||||
}
|
||||
handler: () => this.umpireService.delete(umpire.id)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
private removeUmpire(umpire: Umpire): void {
|
||||
this.umpireService.delete(umpire.id);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,8 @@
|
||||
min="1"
|
||||
labelPlacement="floating"
|
||||
[value]="currentSettings.numberOfCourts"
|
||||
(ionInput)="updateNumberOfCourts($event)">
|
||||
step="1"
|
||||
(ionChange)="updateNumberOfCourts($event)">
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { signal } from '@angular/core';
|
||||
import { SettingsService } from '../services/settings-service';
|
||||
import { Tab3Page } from './tab3.page';
|
||||
|
||||
describe('Tab3Page', () => {
|
||||
let component: Tab3Page;
|
||||
let fixture: ComponentFixture<Tab3Page>;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: SettingsService, useValue: { settings: signal(undefined) } }
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(Tab3Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
+12
-44
@@ -16,12 +16,13 @@ import {
|
||||
IonLabel
|
||||
} from '@ionic/angular';
|
||||
import { SettingsService } from '../services/settings-service';
|
||||
import { CourtUmpireService } from '../services/court.umpire.service';
|
||||
import { CourtServiceJudgeService } from '../services/court.service.judge.service';
|
||||
import { CourtServiceJudge } from 'db';
|
||||
import { WaitingServiceJudgesService } from '../services/waiting-service-judges.service';
|
||||
import { TranslationService } from '../i18n/translation.service';
|
||||
import { Language } from '../i18n/translations';
|
||||
import type {
|
||||
InputCustomEvent,
|
||||
SelectCustomEvent,
|
||||
ToggleCustomEvent
|
||||
} from '@ionic/angular';
|
||||
import { TranslatePipe } from '../i18n/translation.pipe';
|
||||
import { ThemePreference, ThemeService } from '../services/theme.service';
|
||||
|
||||
@@ -50,74 +51,41 @@ import { ThemePreference, ThemeService } from '../services/theme.service';
|
||||
})
|
||||
export class Tab3Page {
|
||||
readonly settingsService = inject(SettingsService);
|
||||
readonly courtUmpireService = inject(CourtUmpireService);
|
||||
readonly courtServiceJudgeService = inject(CourtServiceJudgeService);
|
||||
readonly waitingServiceJudgeService = inject(WaitingServiceJudgesService);
|
||||
readonly translationService = inject(TranslationService);
|
||||
|
||||
readonly themeService = inject(ThemeService);
|
||||
|
||||
readonly settings = this.settingsService.settings;
|
||||
|
||||
async updateNumberOfCourts(event: CustomEvent): Promise<void> {
|
||||
this.courtUmpireService
|
||||
.umpires()
|
||||
.filter((cu) => {
|
||||
return cu.courtNo > Number(event.detail.value);
|
||||
})
|
||||
.map((cu) => {
|
||||
this.courtUmpireService.delete(cu.id);
|
||||
});
|
||||
|
||||
this.courtServiceJudgeService
|
||||
.umpires()
|
||||
.filter((cu) => {
|
||||
return cu.courtNo > Number(event.detail.value);
|
||||
})
|
||||
.map((cu) => {
|
||||
this.courtServiceJudgeService.delete(cu.id);
|
||||
});
|
||||
|
||||
async updateNumberOfCourts(event: InputCustomEvent): Promise<void> {
|
||||
await this.settingsService.update({
|
||||
numberOfCourts: Number(event.detail.value)
|
||||
});
|
||||
}
|
||||
|
||||
async updateWithServiceJudge(event: CustomEvent): Promise<void> {
|
||||
async updateWithServiceJudge(event: ToggleCustomEvent): Promise<void> {
|
||||
await this.settingsService.update({
|
||||
withServiceJudge: event.detail.checked
|
||||
});
|
||||
|
||||
if (!event.detail.checked) {
|
||||
this.courtServiceJudgeService.umpires().map((csj: CourtServiceJudge) => {
|
||||
this.courtServiceJudgeService.delete(csj.id);
|
||||
});
|
||||
|
||||
this.waitingServiceJudgeService.waitingServiceJudges().map((wsj) => {
|
||||
this.waitingServiceJudgeService.remove(wsj.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateWaitingListQuickButtons(
|
||||
event: CustomEvent<{ checked: boolean }>
|
||||
): Promise<void> {
|
||||
async updateWaitingListQuickButtons(event: ToggleCustomEvent): Promise<void> {
|
||||
await this.settingsService.update({
|
||||
waitingListQuickButtons: event.detail.checked
|
||||
});
|
||||
}
|
||||
|
||||
async updateShowAlert(event: CustomEvent): Promise<void> {
|
||||
async updateShowAlert(event: ToggleCustomEvent): Promise<void> {
|
||||
await this.settingsService.update({
|
||||
showAlert: event.detail.checked
|
||||
});
|
||||
}
|
||||
|
||||
updateTheme(event: CustomEvent<{ value: ThemePreference }>): void {
|
||||
updateTheme(event: SelectCustomEvent<ThemePreference>): void {
|
||||
this.themeService.setTheme(event.detail.value);
|
||||
}
|
||||
|
||||
updateLanguage(event: CustomEvent): void {
|
||||
this.translationService.setLanguage(event.detail.value as Language);
|
||||
updateLanguage(event: SelectCustomEvent<Language>): void {
|
||||
this.translationService.setLanguage(event.detail.value);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Component,
|
||||
EnvironmentInjector,
|
||||
inject,
|
||||
ChangeDetectionStrategy
|
||||
} from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import {
|
||||
IonTabs,
|
||||
IonTabBar,
|
||||
@@ -23,8 +18,6 @@ import { TranslatePipe } from '../i18n/translation.pipe';
|
||||
imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, TranslatePipe]
|
||||
})
|
||||
export class TabsPage {
|
||||
public environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
constructor() {
|
||||
addIcons({ gridOutline, peopleOutline, settingsOutline });
|
||||
}
|
||||
|
||||
+12
-17
@@ -10,20 +10,20 @@
|
||||
*/
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
@import "@ionic/angular/css/core.css";
|
||||
@import '@ionic/angular/css/core.css';
|
||||
|
||||
/* Basic CSS for apps built with Ionic */
|
||||
@import "@ionic/angular/css/normalize.css";
|
||||
@import "@ionic/angular/css/structure.css";
|
||||
@import "@ionic/angular/css/typography.css";
|
||||
@import "@ionic/angular/css/display.css";
|
||||
@import '@ionic/angular/css/normalize.css';
|
||||
@import '@ionic/angular/css/structure.css';
|
||||
@import '@ionic/angular/css/typography.css';
|
||||
@import '@ionic/angular/css/display.css';
|
||||
|
||||
/* Optional CSS utils that can be commented out */
|
||||
@import "@ionic/angular/css/padding.css";
|
||||
@import "@ionic/angular/css/float-elements.css";
|
||||
@import "@ionic/angular/css/text-alignment.css";
|
||||
@import "@ionic/angular/css/text-transformation.css";
|
||||
@import "@ionic/angular/css/flex-utils.css";
|
||||
@import '@ionic/angular/css/padding.css';
|
||||
@import '@ionic/angular/css/float-elements.css';
|
||||
@import '@ionic/angular/css/text-alignment.css';
|
||||
@import '@ionic/angular/css/text-transformation.css';
|
||||
@import '@ionic/angular/css/flex-utils.css';
|
||||
|
||||
/**
|
||||
* Ionic Dark Mode
|
||||
@@ -34,12 +34,7 @@
|
||||
|
||||
@import '@ionic/angular/css/palettes/dark.class.css';
|
||||
|
||||
.wide.sc-ion-alert-md-h {
|
||||
.sc-ion-alert-md {
|
||||
ion-alert.wide {
|
||||
--backdrop-opacity: 0.8;
|
||||
}
|
||||
|
||||
&.sc-ion-alert-md-h {
|
||||
--min-width: 400px;
|
||||
}
|
||||
--min-width: min(400px, calc(100vw - 32px));
|
||||
}
|
||||
+18
-10
@@ -1,27 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Court Pilot</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/icon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/icon/favicon-16x16.png">
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="assets/icon/favicon-32x32.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="assets/icon/favicon-16x16.png" />
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+3
-1
@@ -18,4 +18,6 @@ bootstrapApplication(AppComponent, {
|
||||
provideIonicAngular(),
|
||||
provideRouter(routes, withPreloading(PreloadAllModules))
|
||||
]
|
||||
});
|
||||
}).catch((error: unknown) =>
|
||||
console.error('Application bootstrap failed:', error)
|
||||
);
|
||||
+3
-1
@@ -2,4 +2,6 @@
|
||||
* Prevents Angular change detection from
|
||||
* running with certain Web Component callbacks
|
||||
*/
|
||||
(window as any).__Zone_disable_customElements = true;
|
||||
(
|
||||
window as Window & { __Zone_disable_customElements?: boolean }
|
||||
).__Zone_disable_customElements = true;
|
||||
+1
-9
@@ -6,13 +6,5 @@
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"angularCompilerOptions": {
|
||||
"extendedDiagnostics": {
|
||||
"checks": {
|
||||
"nullishCoalescingNotNullable": "suppress",
|
||||
"optionalChainNotNullable": "suppress"
|
||||
}
|
||||
}
|
||||
}
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user