Compare commits
16 Commits
e6a6949e4a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 33d635839f | |||
| abf0cd2359 | |||
| 02fc32942a | |||
| ccfca0dd25 | |||
| 60f12cc333 | |||
| 63737e46eb | |||
| 2fb8d34ec1 | |||
| 12871ef7d2 | |||
| d317ce6179 | |||
| 0c071c0621 | |||
| 71c3733363 | |||
| 03bfc8fc16 | |||
| 56b9a186d4 | |||
| 0f46cb3c93 | |||
| 08d64bbaaf | |||
| a79c082d45 |
@@ -0,0 +1,61 @@
|
|||||||
|
import Dexie, { type EntityTable } from 'dexie';
|
||||||
|
|
||||||
|
export interface Umpire {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
country: string;
|
||||||
|
gender: string;
|
||||||
|
courtNo?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CourtUmpire {
|
||||||
|
id: number;
|
||||||
|
umpireId: number | null;
|
||||||
|
courtNo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CourtServiceJudge {
|
||||||
|
id: number;
|
||||||
|
umpireId: number | null;
|
||||||
|
courtNo: 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;
|
||||||
|
showAlert: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Dexie('CourtPilot') as Dexie & {
|
||||||
|
umpires: EntityTable<Umpire, 'id'>;
|
||||||
|
courtUmpires: EntityTable<CourtUmpire, 'id'>;
|
||||||
|
courtServiceJudges: EntityTable<CourtServiceJudge, 'id'>;
|
||||||
|
settings: EntityTable<Settings, 'id'>;
|
||||||
|
waitingUmpires: EntityTable<WaitingAsUmpire, 'id'>;
|
||||||
|
waitingServiceJudges: EntityTable<WaitingAsServiceJudge, 'id'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
db.version(1).stores({
|
||||||
|
umpires: '++id, lastName',
|
||||||
|
courtUmpires: '++id, courtNo, umpireId',
|
||||||
|
courtServiceJudges: '++id, courtNo, umpireId',
|
||||||
|
settings: '++id',
|
||||||
|
waitingUmpires: '++id, order, umpireId',
|
||||||
|
waitingServiceJudges: '++id, order, serviceJudgeId'
|
||||||
|
});
|
||||||
|
|
||||||
|
export { db };
|
||||||
Generated
+23
-2
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^20.0.0",
|
"@angular/animations": "^20.0.0",
|
||||||
|
"@angular/cdk": "^20.2.14",
|
||||||
"@angular/common": "^20.0.0",
|
"@angular/common": "^20.0.0",
|
||||||
"@angular/compiler": "^20.0.0",
|
"@angular/compiler": "^20.0.0",
|
||||||
"@angular/core": "^20.0.0",
|
"@angular/core": "^20.0.0",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"@capacitor/keyboard": "8.0.3",
|
"@capacitor/keyboard": "8.0.3",
|
||||||
"@capacitor/status-bar": "8.0.2",
|
"@capacitor/status-bar": "8.0.2",
|
||||||
"@ionic/angular": "^8.0.0",
|
"@ionic/angular": "^8.0.0",
|
||||||
|
"dexie": "^4.4.2",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
@@ -728,6 +730,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/cdk": {
|
||||||
|
"version": "20.2.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz",
|
||||||
|
"integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": "^8.0.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^20.0.0 || ^21.0.0",
|
||||||
|
"@angular/core": "^20.0.0 || ^21.0.0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/cli": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "20.3.26",
|
"version": "20.3.26",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.26.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.26.tgz",
|
||||||
@@ -7977,6 +7994,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dexie": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/di": {
|
"node_modules/di": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
|
||||||
@@ -13525,7 +13548,6 @@
|
|||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "^8.0.0"
|
"entities": "^8.0.0"
|
||||||
@@ -13579,7 +13601,6 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^20.0.0",
|
"@angular/animations": "^20.0.0",
|
||||||
|
"@angular/cdk": "^20.2.14",
|
||||||
"@angular/common": "^20.0.0",
|
"@angular/common": "^20.0.0",
|
||||||
"@angular/compiler": "^20.0.0",
|
"@angular/compiler": "^20.0.0",
|
||||||
"@angular/core": "^20.0.0",
|
"@angular/core": "^20.0.0",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"@capacitor/keyboard": "8.0.3",
|
"@capacitor/keyboard": "8.0.3",
|
||||||
"@capacitor/status-bar": "8.0.2",
|
"@capacitor/status-bar": "8.0.2",
|
||||||
"@ionic/angular": "^8.0.0",
|
"@ionic/angular": "^8.0.0",
|
||||||
|
"dexie": "^4.4.2",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
"minHeight": 600,
|
||||||
|
"minWidth": 800,
|
||||||
"title": "Court Pilot",
|
"title": "Court Pilot",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600,
|
"height": 600,
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<div id="container">
|
|
||||||
<strong>{{ name }}</strong>
|
|
||||||
<p>
|
|
||||||
Explore
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href="https://ionicframework.com/docs/components"
|
|
||||||
>UI Components</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#container {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#container strong {
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#container p {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 22px;
|
|
||||||
|
|
||||||
color: #8c8c8c;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#container a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ExploreContainerComponent } from './explore-container.component';
|
|
||||||
|
|
||||||
describe('ExploreContainerComponent', () => {
|
|
||||||
let component: ExploreContainerComponent;
|
|
||||||
let fixture: ComponentFixture<ExploreContainerComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
fixture = TestBed.createComponent(ExploreContainerComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-explore-container',
|
|
||||||
templateUrl: './explore-container.component.html',
|
|
||||||
styleUrls: ['./explore-container.component.scss'],
|
|
||||||
})
|
|
||||||
export class ExploreContainerComponent {
|
|
||||||
@Input() name?: string;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { FullnamePipe } from './fullname-pipe';
|
||||||
|
|
||||||
|
describe('FullnamePipePipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new FullnamePipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { Umpire } from 'db';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'fullname',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class FullnamePipe implements PipeTransform {
|
||||||
|
transform(value: Umpire | undefined, ...args: unknown[]): string {
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// TODO: in case of multilang, change here
|
||||||
|
return value.lastName + ' ' + value.firstName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CourtServiceJudgeService } from './court.service.judge.service';
|
||||||
|
|
||||||
|
describe('CourtServiceJudgeService', () => {
|
||||||
|
let service: CourtServiceJudgeService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(CourtServiceJudgeService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { CourtServiceJudge, db } from 'db';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CourtServiceJudgeService {
|
||||||
|
/**
|
||||||
|
* All service judges (reactive)
|
||||||
|
*/
|
||||||
|
readonly umpires = toSignal(
|
||||||
|
from(liveQuery(() => db.courtServiceJudges.orderBy('courtNo').toArray())),
|
||||||
|
{ initialValue: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get by court number (reactive)
|
||||||
|
*/
|
||||||
|
getByCourtNo(courtNo: number) {
|
||||||
|
return toSignal<CourtServiceJudge | undefined>(
|
||||||
|
liveQuery(() =>
|
||||||
|
db.courtServiceJudges.where('courtNo').equals(courtNo).first()
|
||||||
|
),
|
||||||
|
{ initialValue: undefined }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create / update
|
||||||
|
*/
|
||||||
|
async save(item: Omit<CourtServiceJudge, 'id'>): Promise<number> {
|
||||||
|
return db.courtServiceJudges.add(item as CourtServiceJudge);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
changes: Partial<CourtServiceJudge>
|
||||||
|
): Promise<number> {
|
||||||
|
return db.courtServiceJudges.update(id, changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
await db.courtServiceJudges.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeByUmpireId(umpireId: number): Promise<void> {
|
||||||
|
const item = await db.courtServiceJudges
|
||||||
|
.where('umpireId')
|
||||||
|
.equals(umpireId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!item?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.courtServiceJudges.delete(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CourtUmpireService } from './court.umpire.service';
|
||||||
|
|
||||||
|
describe('CourtUmpireService', () => {
|
||||||
|
let service: CourtUmpireService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(CourtUmpireService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { CourtUmpire, db } from 'db';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CourtUmpireService {
|
||||||
|
/**
|
||||||
|
* All court umpires (reactive)
|
||||||
|
*/
|
||||||
|
readonly umpires = toSignal(
|
||||||
|
from(liveQuery(() => db.courtUmpires.orderBy('courtNo').toArray())),
|
||||||
|
{ initialValue: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get by court number (reactive single)
|
||||||
|
*/
|
||||||
|
getByCourtNo(courtNo: number) {
|
||||||
|
return toSignal<CourtUmpire | undefined>(
|
||||||
|
liveQuery(() => db.courtUmpires.where('courtNo').equals(courtNo).first()),
|
||||||
|
{ initialValue: undefined }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create / update
|
||||||
|
*/
|
||||||
|
async save(item: Omit<CourtUmpire, 'id'>): Promise<number> {
|
||||||
|
return db.courtUmpires.add(item as CourtUmpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, changes: Partial<CourtUmpire>): Promise<number> {
|
||||||
|
return db.courtUmpires.update(id, changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
await db.courtUmpires.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeByUmpireId(umpireId: number): Promise<void> {
|
||||||
|
const item = await db.courtUmpires
|
||||||
|
.where('umpireId')
|
||||||
|
.equals(umpireId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!item?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.courtUmpires.delete(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,47 @@
|
|||||||
|
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,
|
||||||
|
showAlert: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UmpireService } from './umpire.service';
|
||||||
|
|
||||||
|
describe('Umpire', () => {
|
||||||
|
let service: UmpireService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(UmpireService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { db } from 'db';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
import { Umpire } from '../../../db';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UmpireService {
|
||||||
|
/**
|
||||||
|
* Reactive signal with all umpires
|
||||||
|
*/
|
||||||
|
readonly umpires = toSignal(
|
||||||
|
from(liveQuery(() => db.umpires.orderBy('lastName').toArray())),
|
||||||
|
{
|
||||||
|
initialValue: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create
|
||||||
|
*/
|
||||||
|
async create(umpire: Omit<Umpire, 'id'>): Promise<number> {
|
||||||
|
return await db.umpires.add({
|
||||||
|
...umpire
|
||||||
|
} as Umpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one
|
||||||
|
*/
|
||||||
|
async getById(id: number): Promise<Umpire | undefined> {
|
||||||
|
return await db.umpires.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update
|
||||||
|
*/
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
changes: Partial<Omit<Umpire, 'id'>>
|
||||||
|
): Promise<number> {
|
||||||
|
return await db.umpires.update(id, changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete
|
||||||
|
*/
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
await db.umpires.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all
|
||||||
|
*/
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
await db.umpires.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,161 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { db, Umpire } 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, toPosition?: number): Promise<number> {
|
||||||
|
const id = await 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toPosition !== undefined) {
|
||||||
|
await this.moveToPosition(serviceJudgeId, toPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove from queue
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
await db.waitingServiceJudges.delete(id);
|
||||||
|
|
||||||
|
await this.reinitOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeByUmpireId(umpireId: number): Promise<void> {
|
||||||
|
const item = await db.waitingServiceJudges
|
||||||
|
.where('serviceJudgeId')
|
||||||
|
.equals(umpireId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!item?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.remove(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToPosition(umpireId: number, newOrder: number): Promise<void> {
|
||||||
|
await db.transaction('rw', db.waitingServiceJudges, async () => {
|
||||||
|
const current = await db.waitingServiceJudges
|
||||||
|
.where('serviceJudgeId')
|
||||||
|
.equals(umpireId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldOrder = current.order;
|
||||||
|
|
||||||
|
if (oldOrder === newOrder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOrder < oldOrder) {
|
||||||
|
// Moving up: shift affected records down
|
||||||
|
|
||||||
|
const affected = await db.waitingServiceJudges
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.order >= newOrder &&
|
||||||
|
item.order < oldOrder &&
|
||||||
|
item.id !== current.id
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
for (const item of affected) {
|
||||||
|
await db.waitingServiceJudges.update(item.id!, {
|
||||||
|
order: item.order + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Moving down: shift affected records up
|
||||||
|
|
||||||
|
const affected = await db.waitingServiceJudges
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.order > oldOrder &&
|
||||||
|
item.order <= newOrder &&
|
||||||
|
item.id !== current.id
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
for (const item of affected) {
|
||||||
|
await db.waitingServiceJudges.update(item.id!, {
|
||||||
|
order: item.order - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.waitingServiceJudges.update(current.id!, {
|
||||||
|
order: newOrder
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async reinitOrders(): Promise<void> {
|
||||||
|
await db.transaction('rw', db.waitingServiceJudges, async () => {
|
||||||
|
const items = await db.waitingServiceJudges.orderBy('order').toArray();
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
|
||||||
|
const expectedOrder = i + 1;
|
||||||
|
|
||||||
|
if (item.order !== expectedOrder) {
|
||||||
|
await db.waitingServiceJudges.update(item.id!, {
|
||||||
|
order: expectedOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentUmpire(): Promise<Umpire | undefined> {
|
||||||
|
const item = await db.waitingServiceJudges.where('order').equals(1).first();
|
||||||
|
|
||||||
|
return item ? db.umpires.get(item.serviceJudgeId) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,161 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { db, Umpire } 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, toPosition?: number): Promise<number> {
|
||||||
|
const id = await 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toPosition !== undefined) {
|
||||||
|
await this.moveToPosition(umpireId, toPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove from queue
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
await db.waitingUmpires.delete(id);
|
||||||
|
|
||||||
|
await this.reinitOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeByUmpireId(umpireId: number): Promise<void> {
|
||||||
|
const item = await db.waitingUmpires
|
||||||
|
.where('umpireId')
|
||||||
|
.equals(umpireId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!item?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.remove(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToPosition(umpireId: number, newOrder: number): Promise<void> {
|
||||||
|
await db.transaction('rw', db.waitingUmpires, async () => {
|
||||||
|
const current = await db.waitingUmpires
|
||||||
|
.where('umpireId')
|
||||||
|
.equals(umpireId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldOrder = current.order;
|
||||||
|
|
||||||
|
if (oldOrder === newOrder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOrder < oldOrder) {
|
||||||
|
// Moving up: shift affected records down
|
||||||
|
|
||||||
|
const affected = await db.waitingUmpires
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.order >= newOrder &&
|
||||||
|
item.order < oldOrder &&
|
||||||
|
item.id !== current.id
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
for (const item of affected) {
|
||||||
|
await db.waitingUmpires.update(item.id!, {
|
||||||
|
order: item.order + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Moving down: shift affected records up
|
||||||
|
|
||||||
|
const affected = await db.waitingUmpires
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.order > oldOrder &&
|
||||||
|
item.order <= newOrder &&
|
||||||
|
item.id !== current.id
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
for (const item of affected) {
|
||||||
|
await db.waitingUmpires.update(item.id!, {
|
||||||
|
order: item.order - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.waitingUmpires.update(current.id!, {
|
||||||
|
order: newOrder
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async reinitOrders(): Promise<void> {
|
||||||
|
await db.transaction('rw', db.waitingUmpires, async () => {
|
||||||
|
const items = await db.waitingUmpires.orderBy('order').toArray();
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
|
||||||
|
const expectedOrder = i + 1;
|
||||||
|
|
||||||
|
if (item.order !== expectedOrder) {
|
||||||
|
await db.waitingUmpires.update(item.id!, {
|
||||||
|
order: expectedOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentUmpire(): Promise<Umpire | undefined> {
|
||||||
|
const item = await db.waitingUmpires.where('order').equals(1).first();
|
||||||
|
|
||||||
|
return item ? db.umpires.get(item.umpireId) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
-6
@@ -1,17 +1,185 @@
|
|||||||
<ion-header [translucent]="true">
|
<ion-header [translucent]="true">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>
|
<ion-title> Pályák </ion-title>
|
||||||
Tab 1
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content [fullscreen]="true" cdkDropListGroup>
|
||||||
<ion-header collapse="condense">
|
<ion-header collapse="condense">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title size="large">Tab 1</ion-title>
|
<ion-title size="large">Pályák</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<app-explore-container name="Tab 1 page"></app-explore-container>
|
<ion-grid [fixed]="'fixed'" class="bottom-lists">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col [size]="1" class="ion-display-none ion-display-xl-block">
|
||||||
|
<ion-item [lines]="'none'">Pálya</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col [size]="1" class="ion-display-xl-none">
|
||||||
|
<ion-item [lines]="'none'">P</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-item [lines]="'none'">
|
||||||
|
<ion-label class="ion-text-center"> Játékvezető </ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-item [lines]="'none'">
|
||||||
|
<ion-label class="ion-text-center"> Adogatásbíró </ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col size="2" sizeLg="1"></ion-col>
|
||||||
|
</ion-row>
|
||||||
|
@for (item of [].constructor(settings()?.numberOfCourts); track $index) {
|
||||||
|
<ion-row>
|
||||||
|
<ion-col [size]="1">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item [lines]="'none'"> {{ $index + 1 }}. </ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-list
|
||||||
|
[lines]="'none'"
|
||||||
|
cdkDropList
|
||||||
|
[id]="`court-umpire-${$index + 1}`"
|
||||||
|
[cdkDropListData]="umpireByCourt().get($index + 1) ? [umpireByCourt().get($index + 1)] : []"
|
||||||
|
[cdkDropListEnterPredicate]="canDropUmpire"
|
||||||
|
(cdkDropListDropped)="dropToUmpire($event, $index + 1)">
|
||||||
|
@if (umpireByCourt().get($index + 1); as umpire) {
|
||||||
|
|
||||||
|
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||||
|
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-list
|
||||||
|
[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) {
|
||||||
|
|
||||||
|
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||||
|
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col size="2" sizeLg="1">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item lines="none">
|
||||||
|
@if (umpireByCourt().get($index + 1) &&
|
||||||
|
serviceJudgeByCourt().get($index + 1)) {
|
||||||
|
<ion-button
|
||||||
|
[color]="'danger'"
|
||||||
|
(click)="showRemoveConfirmation($index + 1)">
|
||||||
|
<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) {
|
||||||
|
<ion-button
|
||||||
|
[color]="'success'"
|
||||||
|
[disabled]=""
|
||||||
|
(click)="showAddConfirmation($index + 1)">
|
||||||
|
<ion-icon
|
||||||
|
slot="icon-only"
|
||||||
|
size="large"
|
||||||
|
name="enter-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
}
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
}
|
||||||
|
</ion-grid>
|
||||||
|
<ion-grid [fixed]="'fixed'" class="ion-margin-top bottom-lists">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col [size]="4">
|
||||||
|
<ion-item color="primary">
|
||||||
|
<ion-label class="ion-text-center"> Játékvezetők </ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col [size]="4">
|
||||||
|
<ion-item color="primary">
|
||||||
|
<ion-label class="ion-text-center"> Adogatásbírók </ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col [size]="4">
|
||||||
|
<ion-item color="medium">
|
||||||
|
<ion-label class="ion-text-center"> Pihenők </ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<ion-list
|
||||||
|
[lines]="'none'"
|
||||||
|
cdkDropList
|
||||||
|
id="list-waiting-umpires"
|
||||||
|
[cdkDropListData]="waitingUmpires()"
|
||||||
|
(cdkDropListDropped)="dropToWaitingUmpire($event)">
|
||||||
|
@for (umpire of waitingUmpires(); track umpire.id) {
|
||||||
|
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||||
|
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-list
|
||||||
|
[lines]="'none'"
|
||||||
|
cdkDropList
|
||||||
|
id="list-waiting-service-judges"
|
||||||
|
[cdkDropListData]="waitingServiceJudges()"
|
||||||
|
(cdkDropListDropped)="dropToWaitingServiceJudge($event)">
|
||||||
|
@for (umpire of waitingServiceJudges(); track umpire.id) {
|
||||||
|
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||||
|
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-list
|
||||||
|
[lines]="'none'"
|
||||||
|
cdkDropList
|
||||||
|
cdkDropListSortingDisabled
|
||||||
|
id="list-on-rest"
|
||||||
|
(cdkDropListDropped)="dropToRest($event)"
|
||||||
|
[cdkDropListData]="onRest()">
|
||||||
|
@for (umpire of onRest(); track umpire.id) {
|
||||||
|
<div class="custom-ion-item" cdkDrag [cdkDragData]="umpire">
|
||||||
|
<div *cdkDragPlaceholder class="custom-ion-item primary">
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
<ion-label>{{ umpire|fullname }}</ion-label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.bottom-lists ion-list {
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-ion-item {
|
||||||
|
cursor: move;
|
||||||
|
padding: 10px 15px;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: var(--ion-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
+470
-5
@@ -1,13 +1,478 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, computed, inject } from '@angular/core';
|
||||||
import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
|
import {
|
||||||
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
IonGrid,
|
||||||
|
IonCol,
|
||||||
|
IonRow,
|
||||||
|
IonList,
|
||||||
|
IonItem,
|
||||||
|
IonLabel,
|
||||||
|
IonIcon,
|
||||||
|
IonButton,
|
||||||
|
AlertController
|
||||||
|
} from '@ionic/angular/standalone';
|
||||||
|
import { SettingsService } from '../services/settings-service';
|
||||||
|
import { UmpireService } from '../services/umpire.service';
|
||||||
|
import { Umpire } 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,
|
||||||
|
CdkDragPlaceholder,
|
||||||
|
CdkDropList,
|
||||||
|
CdkDropListGroup
|
||||||
|
} from '@angular/cdk/drag-drop';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CourtUmpireService } from '../services/court.umpire.service';
|
||||||
|
import { CourtServiceJudgeService } from '../services/court.service.judge.service';
|
||||||
|
import { addIcons } from 'ionicons';
|
||||||
|
import { enterOutline, exitOutline } from 'ionicons/icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tab1',
|
selector: 'app-tab1',
|
||||||
templateUrl: 'tab1.page.html',
|
templateUrl: 'tab1.page.html',
|
||||||
styleUrls: ['tab1.page.scss'],
|
styleUrls: ['tab1.page.scss'],
|
||||||
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent],
|
providers: [FullnamePipe],
|
||||||
|
imports: [
|
||||||
|
IonButton,
|
||||||
|
IonIcon,
|
||||||
|
IonLabel,
|
||||||
|
IonItem,
|
||||||
|
IonList,
|
||||||
|
IonRow,
|
||||||
|
IonCol,
|
||||||
|
IonGrid,
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
FullnamePipe,
|
||||||
|
CdkDropList,
|
||||||
|
CdkDrag,
|
||||||
|
CommonModule,
|
||||||
|
CdkDragPlaceholder,
|
||||||
|
CdkDropListGroup
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class Tab1Page {
|
export class Tab1Page {
|
||||||
constructor() {}
|
constructor() {
|
||||||
|
addIcons({ exitOutline, enterOutline });
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly settingsService = inject(SettingsService);
|
||||||
|
readonly umpireService = inject(UmpireService);
|
||||||
|
readonly waitingUmpireService = inject(WaitingUmpiresService);
|
||||||
|
readonly waitingServiceJudgeService = inject(WaitingServiceJudgesService);
|
||||||
|
readonly courtUmpireService = inject(CourtUmpireService);
|
||||||
|
readonly courtServiceJudgeService = inject(CourtServiceJudgeService);
|
||||||
|
|
||||||
|
private alertController = inject(AlertController);
|
||||||
|
private fullnamePipe = inject(FullnamePipe);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw signals from services
|
||||||
|
*/
|
||||||
|
readonly settings = this.settingsService.settings;
|
||||||
|
|
||||||
|
readonly _umpires = this.umpireService.umpires;
|
||||||
|
|
||||||
|
readonly _waitingUmpires = this.waitingUmpireService.waitingUmpires;
|
||||||
|
|
||||||
|
readonly _waitingServiceJudges =
|
||||||
|
this.waitingServiceJudgeService.waitingServiceJudges;
|
||||||
|
|
||||||
|
readonly _courtUmpires = this.courtUmpireService.umpires;
|
||||||
|
|
||||||
|
readonly _courtServiceJudges = this.courtServiceJudgeService.umpires;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast O(1) umpire lookup
|
||||||
|
*/
|
||||||
|
readonly umpireMap = computed(() => {
|
||||||
|
return new Map<number, Umpire>(this._umpires().map((u) => [u.id, u]));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waiting umpire IDs
|
||||||
|
*/
|
||||||
|
readonly waitingUmpireIds = computed(() => {
|
||||||
|
return new Set(this._waitingUmpires().map((w) => w.umpireId));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waiting service judge IDs
|
||||||
|
*/
|
||||||
|
readonly waitingServiceJudgeIds = computed(() => {
|
||||||
|
return new Set(this._waitingServiceJudges().map((w) => w.serviceJudgeId));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court umpire IDs
|
||||||
|
*/
|
||||||
|
readonly courtUmpireIds = computed(() => {
|
||||||
|
return new Set(
|
||||||
|
this._courtUmpires()
|
||||||
|
.map((c) => c.umpireId)
|
||||||
|
.filter((id): id is number => id !== null)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court service judge IDs
|
||||||
|
*/
|
||||||
|
readonly courtServiceJudgeIds = computed(() => {
|
||||||
|
return new Set(
|
||||||
|
this._courtServiceJudges()
|
||||||
|
.map((c) => c.umpireId)
|
||||||
|
.filter((id): id is number => id !== null)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Umpires waiting as umpire
|
||||||
|
*/
|
||||||
|
readonly waitingUmpires = computed(() => {
|
||||||
|
const umpireMap = this.umpireMap();
|
||||||
|
|
||||||
|
return this._waitingUmpires()
|
||||||
|
.map((wu) => umpireMap.get(wu.umpireId))
|
||||||
|
.filter((u): u is Umpire => !!u);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Umpires waiting as service judge
|
||||||
|
*/
|
||||||
|
readonly waitingServiceJudges = computed(() => {
|
||||||
|
const umpireMap = this.umpireMap();
|
||||||
|
|
||||||
|
return this._waitingServiceJudges()
|
||||||
|
.map((wsj) => umpireMap.get(wsj.serviceJudgeId))
|
||||||
|
.filter((u): u is Umpire => !!u);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court umpires
|
||||||
|
*/
|
||||||
|
readonly courtUmpires = computed(() => {
|
||||||
|
const umpireMap = this.umpireMap();
|
||||||
|
|
||||||
|
return this._courtUmpires()
|
||||||
|
.map((cu) => {
|
||||||
|
if (cu.umpireId == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return umpireMap.get(cu.umpireId);
|
||||||
|
})
|
||||||
|
.filter((u): u is Umpire => !!u);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court service judges
|
||||||
|
*/
|
||||||
|
readonly courtServiceJudges = computed(() => {
|
||||||
|
const umpireMap = this.umpireMap();
|
||||||
|
|
||||||
|
return this._courtServiceJudges()
|
||||||
|
.map((csj) => {
|
||||||
|
if (csj.umpireId == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return umpireMap.get(csj.umpireId);
|
||||||
|
})
|
||||||
|
.filter((u): u is Umpire => !!u);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Umpires resting
|
||||||
|
*/
|
||||||
|
readonly onRest = computed(() => {
|
||||||
|
const waitingUmpireIds = this.waitingUmpireIds();
|
||||||
|
|
||||||
|
const waitingServiceJudgeIds = this.waitingServiceJudgeIds();
|
||||||
|
|
||||||
|
const courtUmpireIds = this.courtUmpireIds();
|
||||||
|
|
||||||
|
const courtServiceJudgeIds = this.courtServiceJudgeIds();
|
||||||
|
|
||||||
|
return this._umpires().filter((umpire) => {
|
||||||
|
return (
|
||||||
|
!waitingUmpireIds.has(umpire.id) &&
|
||||||
|
!waitingServiceJudgeIds.has(umpire.id) &&
|
||||||
|
!courtUmpireIds.has(umpire.id) &&
|
||||||
|
!courtServiceJudgeIds.has(umpire.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court -> umpire map
|
||||||
|
*/
|
||||||
|
readonly umpireByCourt = computed(() => {
|
||||||
|
const map = new Map<number, Umpire>();
|
||||||
|
|
||||||
|
const umpireMap = this.umpireMap();
|
||||||
|
|
||||||
|
for (const item of this._courtUmpires()) {
|
||||||
|
if (item.umpireId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const umpire = umpireMap.get(item.umpireId);
|
||||||
|
|
||||||
|
if (!umpire) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(item.courtNo, umpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court -> service judge map
|
||||||
|
*/
|
||||||
|
readonly serviceJudgeByCourt = computed(() => {
|
||||||
|
const map = new Map<number, Umpire>();
|
||||||
|
|
||||||
|
const umpireMap = this.umpireMap();
|
||||||
|
|
||||||
|
for (const item of this._courtServiceJudges()) {
|
||||||
|
if (item.umpireId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const umpire = umpireMap.get(item.umpireId);
|
||||||
|
|
||||||
|
if (!umpire) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(item.courtNo, umpire);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Court indexes for template
|
||||||
|
*/
|
||||||
|
readonly courtIndexes = computed(() => {
|
||||||
|
const count = this.settings()?.numberOfCourts ?? 0;
|
||||||
|
|
||||||
|
return Array.from({ length: count }, (_, i) => i + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropToRest(event: CdkDragDrop<Umpire[]>) {
|
||||||
|
if (event.previousContainer === event.container) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const comingFrom = event.previousContainer.id;
|
||||||
|
const umpireToMove = event.item.data;
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
const comingFrom = event.previousContainer.id;
|
||||||
|
this.waitingUmpireService.add(umpireToMove.id, event.currentIndex + 1);
|
||||||
|
|
||||||
|
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropToUmpire(event: CdkDragDrop<(Umpire | undefined)[]>, courtNo: number) {
|
||||||
|
const targetUmpires = event.container.data;
|
||||||
|
|
||||||
|
if (targetUmpires.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comingFrom = event.previousContainer.id;
|
||||||
|
const umpireToMove = event.item.data;
|
||||||
|
this.courtUmpireService.save({ umpireId: umpireToMove.id, courtNo });
|
||||||
|
|
||||||
|
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
dropToServiceJudge(
|
||||||
|
event: CdkDragDrop<(Umpire | undefined)[]>,
|
||||||
|
courtNo: number
|
||||||
|
) {
|
||||||
|
const targetUmpires = event.container.data;
|
||||||
|
|
||||||
|
if (targetUmpires.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comingFrom = event.previousContainer.id;
|
||||||
|
const umpireToMove = event.item.data;
|
||||||
|
this.courtServiceJudgeService.save({ umpireId: umpireToMove.id, courtNo });
|
||||||
|
|
||||||
|
this.removeFromOriginalPlace(umpireToMove, comingFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
public async showRemoveConfirmation(courtNo: number) {
|
||||||
|
const umpire = this.umpireByCourt().get(courtNo);
|
||||||
|
const serviceJudge = this.serviceJudgeByCourt().get(courtNo);
|
||||||
|
|
||||||
|
if (typeof umpire === 'undefined' || typeof serviceJudge === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.settings()?.showAlert) {
|
||||||
|
this.removeUmpiresFromCourt(courtNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alert = await this.alertController.create({
|
||||||
|
header: `Pálya ${courtNo}`,
|
||||||
|
subHeader: 'Biztos leveszed őket pályáról?',
|
||||||
|
cssClass: 'wide',
|
||||||
|
message: `${this.fullnamePipe.transform(umpire)} - ${this.fullnamePipe.transform(serviceJudge)}`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Nem',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Igen',
|
||||||
|
role: 'confirm',
|
||||||
|
handler: () => {
|
||||||
|
this.removeUmpiresFromCourt(courtNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async showAddConfirmation(courtNo: number) {
|
||||||
|
const umpire = await this.waitingUmpireService.getCurrentUmpire();
|
||||||
|
const serviceJudge =
|
||||||
|
await this.waitingServiceJudgeService.getCurrentUmpire();
|
||||||
|
|
||||||
|
if (typeof umpire === 'undefined' || typeof serviceJudge === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.settings()?.showAlert) {
|
||||||
|
this.addUmpiresToCourt(courtNo, umpire, serviceJudge);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alert = await this.alertController.create({
|
||||||
|
header: `Pálya ${courtNo}`,
|
||||||
|
cssClass: 'wide',
|
||||||
|
subHeader: 'Biztos pályára küldöd őket?',
|
||||||
|
message: `${this.fullnamePipe.transform(umpire)} - ${this.fullnamePipe.transform(serviceJudge)}`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Nem',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Igen',
|
||||||
|
role: 'confirm',
|
||||||
|
handler: () => {
|
||||||
|
this.addUmpiresToCourt(courtNo, umpire, serviceJudge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,99 @@
|
|||||||
<ion-header [translucent]="true">
|
<ion-header [translucent]="true">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>
|
<ion-title> Játékvezetők </ion-title>
|
||||||
Tab 2
|
<ion-buttons [slot]="'end'">
|
||||||
</ion-title>
|
<ion-button [color]="'primary'" [fill]="'solid'" id="open-modal">
|
||||||
|
<ion-icon slot="start" name="add"></ion-icon>
|
||||||
|
Hozzáadás
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content [fullscreen]="true">
|
||||||
<ion-header collapse="condense">
|
<ion-header collapse="condense">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title size="large">Tab 2</ion-title>
|
<ion-title size="large">Játékvezetők</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<app-explore-container name="Tab 2 page"></app-explore-container>
|
<ion-grid>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col size="12" sizeMd="6" pushMd="3">
|
||||||
|
<ion-list>
|
||||||
|
@for (umpire of umpires(); track umpire.id) {
|
||||||
|
<ion-item>
|
||||||
|
<ion-label> {{ umpire | fullname }} </ion-label>
|
||||||
|
<ion-button
|
||||||
|
[color]="'danger'"
|
||||||
|
slot="end"
|
||||||
|
(click)="showDeleteConfirmation(umpire)">
|
||||||
|
<ion-icon slot="icon-only" name="trash"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-item>
|
||||||
|
} @empty {
|
||||||
|
<ion-item> Nincsenek játékvezetők </ion-item>
|
||||||
|
}
|
||||||
|
</ion-list>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
|
<ion-modal trigger="open-modal" (willDismiss)="onWillDismiss($event)">
|
||||||
|
<ng-template>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-button (click)="cancel()">Mégse</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>Új játékvezető</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button
|
||||||
|
[disabled]="!isValidUmpire()"
|
||||||
|
(click)="confirm()"
|
||||||
|
[strong]="true">
|
||||||
|
Mentés
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-input
|
||||||
|
label="Vezetéknév"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
placeholder="Vezetéknév"
|
||||||
|
[(ngModel)]="lastName" />
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-input
|
||||||
|
label="Keresztnév"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
placeholder="Keresztnév"
|
||||||
|
[(ngModel)]="firstName" />
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-input
|
||||||
|
label="Ország"
|
||||||
|
labelPlacement="stacked"
|
||||||
|
type="text"
|
||||||
|
placeholder="Ország"
|
||||||
|
[(ngModel)]="country" />
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-select label="Nem" placeholder="Válassz" [(ngModel)]="gender">
|
||||||
|
@for (item of genders; track item.code) {
|
||||||
|
<ion-select-option [value]="item.code">
|
||||||
|
{{item.label}}
|
||||||
|
</ion-select-option>
|
||||||
|
}
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
||||||
|
</ng-template>
|
||||||
|
</ion-modal>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
+132
-5
@@ -1,15 +1,142 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
|
import {
|
||||||
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
IonButtons,
|
||||||
|
IonButton,
|
||||||
|
IonIcon,
|
||||||
|
IonModal,
|
||||||
|
IonItem,
|
||||||
|
IonInput,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonList,
|
||||||
|
IonGrid,
|
||||||
|
IonRow,
|
||||||
|
IonCol,
|
||||||
|
IonLabel,
|
||||||
|
AlertController
|
||||||
|
} from '@ionic/angular/standalone';
|
||||||
|
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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tab2',
|
selector: 'app-tab2',
|
||||||
templateUrl: 'tab2.page.html',
|
templateUrl: 'tab2.page.html',
|
||||||
styleUrls: ['tab2.page.scss'],
|
styleUrls: ['tab2.page.scss'],
|
||||||
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent]
|
providers: [FullnamePipe],
|
||||||
|
imports: [
|
||||||
|
IonLabel,
|
||||||
|
IonCol,
|
||||||
|
IonRow,
|
||||||
|
IonGrid,
|
||||||
|
IonInput,
|
||||||
|
IonItem,
|
||||||
|
IonModal,
|
||||||
|
IonIcon,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
FormsModule,
|
||||||
|
IonSelect,
|
||||||
|
IonSelectOption,
|
||||||
|
IonList,
|
||||||
|
FullnamePipe
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class Tab2Page {
|
export class Tab2Page {
|
||||||
|
@ViewChild(IonModal) modal!: IonModal;
|
||||||
|
|
||||||
constructor() {}
|
public readonly umpires = this.umpireService.umpires;
|
||||||
|
|
||||||
|
public lastName: string = '';
|
||||||
|
public firstName: string = '';
|
||||||
|
public country: string = '';
|
||||||
|
public gender: string = '';
|
||||||
|
|
||||||
|
public genders = [
|
||||||
|
{ label: 'gender.male', code: 'M' },
|
||||||
|
{ label: 'gender.female', code: 'W' }
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private umpireService: UmpireService,
|
||||||
|
private alertController: AlertController,
|
||||||
|
private fullnamePipe: FullnamePipe
|
||||||
|
) {
|
||||||
|
addIcons({ add, trash });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onWillDismiss(event: CustomEvent<OverlayEventDetail>) {
|
||||||
|
if (event.detail.role !== 'confirm') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUmpire: Umpire = event.detail.data;
|
||||||
|
|
||||||
|
await this.umpireService.create({
|
||||||
|
firstName: newUmpire.firstName,
|
||||||
|
lastName: newUmpire.lastName,
|
||||||
|
country: newUmpire.country,
|
||||||
|
gender: newUmpire.gender
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.modal.dismiss(null, 'cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.modal.dismiss(
|
||||||
|
{
|
||||||
|
lastName: this.lastName,
|
||||||
|
firstName: this.firstName,
|
||||||
|
country: this.country,
|
||||||
|
gender: this.gender
|
||||||
|
},
|
||||||
|
'confirm'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidUmpire(): boolean {
|
||||||
|
return this.lastName !== '' && this.firstName !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async showDeleteConfirmation(umpire: Umpire) {
|
||||||
|
const alert = await this.alertController.create({
|
||||||
|
header: 'Megerősítés',
|
||||||
|
subHeader: 'Biztos törlöd a következő játékvezetőt?',
|
||||||
|
message: this.fullnamePipe.transform(umpire),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Nem',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Igen',
|
||||||
|
role: 'confirm',
|
||||||
|
handler: () => {
|
||||||
|
// TODO: also remove from first tab
|
||||||
|
this.removeUmpire(umpire);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeUmpire(umpire: Umpire): void {
|
||||||
|
this.umpireService.delete(umpire.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-11
@@ -1,17 +1,46 @@
|
|||||||
<ion-header [translucent]="true">
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>
|
<ion-title>Beállítások</ion-title>
|
||||||
Tab 3
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content>
|
||||||
<ion-header collapse="condense">
|
<ion-grid [fixed]="true">
|
||||||
<ion-toolbar>
|
<ion-row>
|
||||||
<ion-title size="large">Tab 3</ion-title>
|
<ion-col sizeLg="6" pushLg="3">
|
||||||
</ion-toolbar>
|
@if (settings(); as currentSettings) {
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<app-explore-container name="Tab 3 page"></app-explore-container>
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-toggle
|
||||||
|
[checked]="currentSettings.withServiceJudge"
|
||||||
|
(ionChange)="updateWithServiceJudge($event)">
|
||||||
|
Adogatásbíró
|
||||||
|
</ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-toggle
|
||||||
|
[checked]="currentSettings.showAlert"
|
||||||
|
(ionChange)="updateShowAlert($event)">
|
||||||
|
Figyelmeztetések megjelenítése a gyors gomboknál
|
||||||
|
</ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-input
|
||||||
|
label="Pályák száma"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
labelPlacement="floating"
|
||||||
|
[value]="currentSettings.numberOfCourts"
|
||||||
|
(ionInput)="updateNumberOfCourts($event)">
|
||||||
|
</ion-input>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
}
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,13 +1,79 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
|
import {
|
||||||
import { ExploreContainerComponent } from '../explore-container/explore-container.component';
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
IonList,
|
||||||
|
IonItem,
|
||||||
|
IonToggle,
|
||||||
|
IonInput,
|
||||||
|
IonGrid,
|
||||||
|
IonCol,
|
||||||
|
IonRow
|
||||||
|
} from '@ionic/angular/standalone';
|
||||||
|
import { SettingsService } from '../services/settings-service';
|
||||||
|
import { CourtUmpireService } from '../services/court.umpire.service';
|
||||||
|
import { CourtServiceJudgeService } from '../services/court.service.judge.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tab3',
|
selector: 'app-tab3',
|
||||||
templateUrl: 'tab3.page.html',
|
templateUrl: 'tab3.page.html',
|
||||||
styleUrls: ['tab3.page.scss'],
|
styleUrls: ['tab3.page.scss'],
|
||||||
imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent],
|
imports: [
|
||||||
|
IonRow,
|
||||||
|
IonCol,
|
||||||
|
IonGrid,
|
||||||
|
IonInput,
|
||||||
|
IonToggle,
|
||||||
|
IonItem,
|
||||||
|
IonList,
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class Tab3Page {
|
export class Tab3Page {
|
||||||
constructor() {}
|
readonly settingsService = inject(SettingsService);
|
||||||
|
readonly courtUmpireService = inject(CourtUmpireService);
|
||||||
|
readonly courtServiceJudgeService = inject(CourtServiceJudgeService);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.settingsService.update({
|
||||||
|
numberOfCourts: Number(event.detail.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWithServiceJudge(event: CustomEvent): Promise<void> {
|
||||||
|
await this.settingsService.update({
|
||||||
|
withServiceJudge: event.detail.checked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateShowAlert(event: CustomEvent): Promise<void> {
|
||||||
|
await this.settingsService.update({
|
||||||
|
showAlert: event.detail.checked
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<ion-tabs>
|
<ion-tabs>
|
||||||
<ion-tab-bar slot="bottom">
|
<ion-tab-bar slot="bottom">
|
||||||
<ion-tab-button tab="pilot" href="/tabs/pilot">
|
<ion-tab-button tab="pilot" href="/tabs/pilot">
|
||||||
<ion-icon aria-hidden="true" name="triangle"></ion-icon>
|
<ion-icon aria-hidden="true" name="grid-outline"></ion-icon>
|
||||||
<ion-label>Tab 1</ion-label>
|
<ion-label>Pályák</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
|
|
||||||
<ion-tab-button tab="umpires" href="/tabs/umpires">
|
<ion-tab-button tab="umpires" href="/tabs/umpires">
|
||||||
<ion-icon aria-hidden="true" name="ellipse"></ion-icon>
|
<ion-icon aria-hidden="true" name="people-outline"></ion-icon>
|
||||||
<ion-label>Tab 2</ion-label>
|
<ion-label>Játékvezetők</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
|
|
||||||
<ion-tab-button tab="stats" href="/tabs/stats">
|
<ion-tab-button tab="settings" href="/tabs/settings">
|
||||||
<ion-icon aria-hidden="true" name="square"></ion-icon>
|
<ion-icon aria-hidden="true" name="settings-outline"></ion-icon>
|
||||||
<ion-label>Tab 3</ion-label>
|
<ion-label>Beállítások</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-tab-bar>
|
</ion-tab-bar>
|
||||||
</ion-tabs>
|
</ion-tabs>
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { Component, EnvironmentInjector, inject } from '@angular/core';
|
import { Component, EnvironmentInjector, inject } from '@angular/core';
|
||||||
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular/standalone';
|
import {
|
||||||
|
IonTabs,
|
||||||
|
IonTabBar,
|
||||||
|
IonTabButton,
|
||||||
|
IonIcon,
|
||||||
|
IonLabel
|
||||||
|
} from '@ionic/angular/standalone';
|
||||||
import { addIcons } from 'ionicons';
|
import { addIcons } from 'ionicons';
|
||||||
import { triangle, ellipse, square } from 'ionicons/icons';
|
import { gridOutline, peopleOutline, settingsOutline } from 'ionicons/icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tabs',
|
selector: 'app-tabs',
|
||||||
templateUrl: 'tabs.page.html',
|
templateUrl: 'tabs.page.html',
|
||||||
styleUrls: ['tabs.page.scss'],
|
styleUrls: ['tabs.page.scss'],
|
||||||
imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel],
|
imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel]
|
||||||
})
|
})
|
||||||
export class TabsPage {
|
export class TabsPage {
|
||||||
public environmentInjector = inject(EnvironmentInjector);
|
public environmentInjector = inject(EnvironmentInjector);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
addIcons({ triangle, ellipse, square });
|
addIcons({ gridOutline, peopleOutline, settingsOutline });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const routes: Routes = [
|
|||||||
loadComponent: () => import('../tab2/tab2.page').then((m) => m.Tab2Page)
|
loadComponent: () => import('../tab2/tab2.page').then((m) => m.Tab2Page)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stats',
|
path: 'settings',
|
||||||
loadComponent: () => import('../tab3/tab3.page').then((m) => m.Tab3Page)
|
loadComponent: () => import('../tab3/tab3.page').then((m) => m.Tab3Page)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 346 B |
Binary file not shown.
|
After Width: | Height: | Size: 690 B |
Binary file not shown.
|
Before Width: | Height: | Size: 930 B |
@@ -35,3 +35,13 @@
|
|||||||
/* @import "@ionic/angular/css/palettes/dark.always.css"; */
|
/* @import "@ionic/angular/css/palettes/dark.always.css"; */
|
||||||
/* @import "@ionic/angular/css/palettes/dark.class.css"; */
|
/* @import "@ionic/angular/css/palettes/dark.class.css"; */
|
||||||
@import '@ionic/angular/css/palettes/dark.system.css';
|
@import '@ionic/angular/css/palettes/dark.system.css';
|
||||||
|
|
||||||
|
.wide.sc-ion-alert-md-h {
|
||||||
|
.sc-ion-alert-md {
|
||||||
|
--backdrop-opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sc-ion-alert-md-h {
|
||||||
|
--min-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Ionic App</title>
|
<title>Court Pilot</title>
|
||||||
|
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name="msapplication-tap-highlight" content="no" />
|
<meta name="msapplication-tap-highlight" content="no" />
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="assets/icon/favicon.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 -->
|
<!-- add to homescreen for ios -->
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
// For information on how to create your own theme, please refer to:
|
// For information on how to create your own theme, please refer to:
|
||||||
// https://ionicframework.com/docs/theming/
|
// https://ionicframework.com/docs/theming/
|
||||||
|
@import '@ionic/angular/css/palettes/dark.always.css';
|
||||||
Reference in New Issue
Block a user