1975 lines
72 KiB
TypeScript
Executable File
1975 lines
72 KiB
TypeScript
Executable File
import {
|
|
Component,
|
|
Input,
|
|
OnInit,
|
|
OnDestroy,
|
|
NgZone,
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef
|
|
} from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { Subscription } from 'rxjs';
|
|
import { SelectionService, SelectionData } from '../selection.service/selection.service';
|
|
import { SharedStateService } from '../../service/shared-state.service';
|
|
import { LabelRestrictionService } from '../selection.service/label-restriction.service';
|
|
import { StopbetService } from '../../service/stopbet.service';
|
|
import _ from 'lodash';
|
|
|
|
@Component({
|
|
selector: 'app-touch-pad-menu',
|
|
standalone: true,
|
|
imports: [CommonModule],
|
|
templateUrl: './touch-pad-menu.component.html',
|
|
styleUrls: ['./touch-pad-menu.component.css'],
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
})
|
|
export class TouchPadMenuComponent implements OnInit, OnDestroy {
|
|
@Input() ticketingActive: boolean = false;
|
|
|
|
public twoGroupLabels = ['FRP', 'QNP'];
|
|
public multiLegLabels = ['TBP', 'MJP', 'JPP'];
|
|
public threeGroupLabels = ['TNP'];
|
|
public allowedFieldLabels = ['WNP', 'SHP', 'THP', 'PLP', 'SHW'];
|
|
|
|
labels: string[] = [
|
|
'WNP', 'SHP', 'THP', 'PLP', 'SHW', 'FRP',
|
|
'QNP', 'TNP', 'EXA', 'WSP', 'TBP', 'MJP',
|
|
'JPP', 'SJP', '.'
|
|
];
|
|
|
|
numbers: number[] = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
runnerCount: number = 12;
|
|
labelRowsFlat: string[] = [];
|
|
numbersFlat: number[] = [];
|
|
blockedLabels = new Set<string>();
|
|
actualRunners: Set<number> = new Set();
|
|
wspTicketStage: number = 0;
|
|
selectedLabel: string | null = null;
|
|
selectedNumbers: (number | string)[] = [];
|
|
padValue: string = '';
|
|
canPrint = false;
|
|
calculatorOpen = false;
|
|
calcDisplay = '';
|
|
maxRowsReached: boolean = false;
|
|
totalAmountLimitReached: boolean = false;
|
|
showLimitPopup: boolean = false;
|
|
disabledLabels: string[] = ['SHW', 'SJP', '.','EXA'];
|
|
|
|
// TNP logic
|
|
tanGroupStage = 0;
|
|
tanGroups: (number | string)[][] = [[], [], []];
|
|
|
|
// FRP/QNP logic
|
|
isFirstGroupComplete = false;
|
|
firstGroup: (number | string)[] = [];
|
|
secondGroup: (number | string)[] = [];
|
|
|
|
// Multi-leg logic
|
|
multiLegStage = 0;
|
|
multiLegGroups: (number | string)[][] = [[], [], [], [], []];
|
|
multiLegBaseRaceIdx: number = 0; // 1-based index from structuredRaceCard pools
|
|
currentLegRaceDisplay: string = '';
|
|
currentPool: string | null = null; // exact pool key as present in structuredRaceCard (e.g. 'TBP1','MJP1','JPP1')
|
|
|
|
isBoxed: boolean = false;
|
|
|
|
// FIELD modal
|
|
fieldModalOpen = false;
|
|
fieldInput: string = '';
|
|
fieldFEntered = false;
|
|
|
|
// POOL REPLACE modal
|
|
poolReplaceOpen = false;
|
|
poolReplaceOptions: string[] = [];
|
|
|
|
// TRE popup
|
|
trePopupVisible = false;
|
|
|
|
private currentRowSubscription: Subscription | null = null;
|
|
private selectionsSubscription: Subscription | null = null;
|
|
private runnerCountSubscription: Subscription | null = null;
|
|
private stopbetSubscription: Subscription | null = null;
|
|
private currentTotal: number = 0;
|
|
private currentSelections: SelectionData[] = [];
|
|
|
|
enabledHorseNumbers: number[] = [];
|
|
prevEnabledKey: string = '';
|
|
|
|
btid: string | null = null;
|
|
raceCardData: any = {};
|
|
structuredRaceCard: any = {};
|
|
|
|
selectedRaceNumber: string = '1';
|
|
|
|
private stopbetStatuses: Map<number, string> = new Map();
|
|
|
|
constructor(
|
|
private selectionService: SelectionService,
|
|
private sharedStateService: SharedStateService,
|
|
private labelRestrictionService: LabelRestrictionService,
|
|
private stopbetService: StopbetService,
|
|
private ngZone: NgZone,
|
|
private cdr: ChangeDetectorRef
|
|
) {}
|
|
|
|
ngOnInit() {
|
|
// Always prefer rpinfo.structuredRaceCard if present, else fall back to raceCardData key (but still use only structuredRaceCard content)
|
|
const rpinfo = this.safeGetJSON('rpinfo');
|
|
if (rpinfo && rpinfo.structuredRaceCard) {
|
|
this.structuredRaceCard = rpinfo.structuredRaceCard;
|
|
} else {
|
|
const rc = this.safeGetJSON('raceCardData');
|
|
this.structuredRaceCard = (rc && rc.structuredRaceCard) ? rc.structuredRaceCard : {};
|
|
}
|
|
this.raceCardData = this.structuredRaceCard;
|
|
|
|
// Subscribe to stopbet statuses
|
|
this.stopbetSubscription = this.stopbetService.getStopbetStatuses().subscribe((statuses) => {
|
|
this.stopbetStatuses = new Map(statuses);
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
this.setActualRunners();
|
|
this.cdr.markForCheck();
|
|
});
|
|
|
|
this.runnerCountSubscription = this.sharedStateService.runnerCount$.subscribe((count: number) => {
|
|
this.runnerCount = count || 12;
|
|
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
this.numbersFlat = this.numberRows.flat();
|
|
this.updateLegRaceDisplay(this.currentPool || '');
|
|
this.btid = localStorage.getItem('btid');
|
|
this.setActualRunners();
|
|
this.cdr.markForCheck();
|
|
});
|
|
|
|
this.labelRowsFlat = this.labelRows.flat();
|
|
|
|
this.selectionsSubscription = this.selectionService.selections$.subscribe((selections: SelectionData[]) => {
|
|
this.currentSelections = selections;
|
|
this.maxRowsReached = selections.length >= 5;
|
|
const totalAmount = selections.reduce((sum: number, selection: SelectionData) => sum + (selection.total || 0), 0);
|
|
this.totalAmountLimitReached = totalAmount >= 5000;
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
if (!this.totalAmountLimitReached) {
|
|
this.showLimitPopup = false;
|
|
}
|
|
this.cdr.markForCheck();
|
|
});
|
|
|
|
this.currentRowSubscription = this.selectionService.currentRow$.subscribe((row: SelectionData) => {
|
|
this.currentTotal = row.total || 0;
|
|
this.cdr.markForCheck();
|
|
});
|
|
|
|
this.sharedStateService.selectedRace$.subscribe((race: number) => {
|
|
this.selectedRaceNumber = String(race || '1');
|
|
if (this.currentPool && this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
this.updateLegRaceDisplay(this.currentPool);
|
|
const runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, this.multiLegStage);
|
|
this.runnerCount = runnerCount || 12;
|
|
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
this.numbersFlat = this.numberRows.flat();
|
|
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
|
|
} else {
|
|
this.setActualRunners();
|
|
}
|
|
this.cdr.markForCheck();
|
|
});
|
|
|
|
// Keep raw storage copy if present (but all logic below uses structuredRaceCard)
|
|
const data = localStorage.getItem('raceCardData');
|
|
if (data) {
|
|
try { this.raceCardData = JSON.parse(data); } catch { this.raceCardData = this.raceCardData || {}; }
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.currentRowSubscription?.unsubscribe();
|
|
this.selectionsSubscription?.unsubscribe();
|
|
this.runnerCountSubscription?.unsubscribe();
|
|
this.stopbetSubscription?.unsubscribe();
|
|
}
|
|
|
|
private safeGetJSON(key: string): any | null {
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) return null;
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- STRICT pool/key mapping using structuredRaceCard only ---
|
|
private normalizePoolNameToKey(name: string | null | undefined): string | null {
|
|
if (!name) return null;
|
|
const n = String(name).trim();
|
|
// Expect caller to pass canonical pool names like 'mjp1','jkp1','trb1' or uppercase 'MJP1' etc.
|
|
// Normalize to uppercase keys used in structuredRaceCard.pools (e.g., 'TBP1','MJP1','JPP1')
|
|
const lower = n.toLowerCase();
|
|
if (lower.startsWith('tbp') || lower.startsWith('trb')) {
|
|
const possible = ['TBP1', 'TBP2', 'TRB1', 'TRB2'];
|
|
return this.findExistingPoolKey(possible) || n.toUpperCase();
|
|
}
|
|
if (lower.startsWith('mjp')) {
|
|
const possible = ['MJP1', 'MJP2'];
|
|
return this.findExistingPoolKey(possible) || n.toUpperCase();
|
|
}
|
|
if (lower.startsWith('jkp') || lower.startsWith('jpp')) {
|
|
const possible = ['JPP1', 'JPP2', 'JKP1', 'JKP2'];
|
|
return this.findExistingPoolKey(possible) || n.toUpperCase();
|
|
}
|
|
return n.toUpperCase();
|
|
}
|
|
|
|
private findExistingPoolKey(possibleKeys: string[]): string | null {
|
|
const pools = this.structuredRaceCard?.pools || {};
|
|
for (const k of possibleKeys) {
|
|
if (k in pools) return k;
|
|
}
|
|
const poolKeys = Object.keys(pools || {});
|
|
for (const k of possibleKeys) {
|
|
const lc = k.toLowerCase();
|
|
const found = poolKeys.find(pk => pk.toLowerCase().includes(lc));
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// --- RACE/POOL LOOKUPS: use structuredRaceCard only (no static fallbacks) ---
|
|
private getRaceIndicesForPoolExact(poolKey: string): number[] {
|
|
const pools = this.structuredRaceCard?.pools || {};
|
|
return Array.isArray(pools?.[poolKey]) ? pools[poolKey].slice() : [];
|
|
}
|
|
|
|
private getRaceForLegExact(poolKey: string, leg: number): number | null {
|
|
const combo = this.structuredRaceCard?.comboRaces || {};
|
|
if (Array.isArray(combo?.[poolKey]) && combo[poolKey].length > leg) {
|
|
return combo[poolKey][leg];
|
|
}
|
|
const pools = this.structuredRaceCard?.pools || {};
|
|
if (Array.isArray(pools?.[poolKey]) && pools[poolKey].length > leg) {
|
|
return pools[poolKey][leg];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private getBaseRaceIndexForPoolExact(poolKey: string): number | null {
|
|
const poolRaces = this.getRaceIndicesForPoolExact(poolKey);
|
|
if (poolRaces.length > 0) return poolRaces[0];
|
|
return null;
|
|
}
|
|
|
|
private getRunnerCountForLegExact(raceNo: number | null): number {
|
|
if (raceNo === null) return 0;
|
|
const races = this.structuredRaceCard?.raceVenueRaces?.races || [];
|
|
const race = races.find((r: any) => Number(r?.raceNo) === Number(raceNo));
|
|
if (!race) return 0;
|
|
if (Array.isArray(race.horses)) return race.horses.length;
|
|
return 0;
|
|
}
|
|
|
|
getActualRunnersForCurrentPoolLeg(): Set<number> {
|
|
if (!this.currentPool) return new Set();
|
|
const poolKey = this.normalizePoolNameToKey(this.currentPool);
|
|
if (!poolKey) return new Set();
|
|
|
|
const raceNo = this.getRaceForLegExact(poolKey, this.multiLegStage);
|
|
if (raceNo === null) return new Set();
|
|
|
|
const stop = this.stopbetStatuses.get(raceNo);
|
|
if (stop === 'Y') return new Set();
|
|
|
|
const races = this.structuredRaceCard?.raceVenueRaces?.races || [];
|
|
const race = races.find((r: any) => Number(r?.raceNo) === Number(raceNo));
|
|
if (!race || !Array.isArray(race.horses)) return new Set();
|
|
return new Set(race.horses.map((n: any) => Number(n)));
|
|
}
|
|
|
|
getActualRunnersForCurrentRace(): Set<number> {
|
|
const races = this.structuredRaceCard?.raceVenueRaces?.races || [];
|
|
const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10);
|
|
const race = races.find((r: any) => Number(r?.raceNo) === Number(selectedRaceIdx));
|
|
if (!race || !Array.isArray(race.horses)) return new Set();
|
|
const stop = this.stopbetStatuses.get(selectedRaceIdx);
|
|
if (stop === 'Y') return new Set();
|
|
return new Set(race.horses.map((n: any) => Number(n)));
|
|
}
|
|
|
|
setActualRunners() {
|
|
if (this.currentPool && this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
|
|
const raceNo = this.getRaceForLegExact(this.currentPool || '', this.multiLegStage);
|
|
this.runnerCount = this.getRunnerCountForLegExact(raceNo);
|
|
} else {
|
|
this.actualRunners = this.getActualRunnersForCurrentRace();
|
|
const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10);
|
|
this.runnerCount = this.getRunnerCountForLegExact(selectedRaceIdx);
|
|
}
|
|
}
|
|
|
|
getHorseNumbersForSelectedRace(): number[] {
|
|
const races = this.structuredRaceCard?.raceVenueRaces?.races || [];
|
|
const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10);
|
|
const race = races.find((r: any) => Number(r?.raceNo) === Number(selectedRaceIdx));
|
|
if (!race || !Array.isArray(race.horses)) return [];
|
|
return race.horses.map((n: any) => Number(n));
|
|
}
|
|
|
|
getHorseNumbersForRaceIdx(raceNo: number): number[] {
|
|
const races = this.structuredRaceCard?.raceVenueRaces?.races || [];
|
|
const race = races.find((r: any) => Number(r?.raceNo) === Number(raceNo));
|
|
if (!race || !Array.isArray(race.horses)) return [];
|
|
return race.horses.map((n: any) => Number(n));
|
|
}
|
|
|
|
get labelRows() {
|
|
return this.chunk(this.labels, 3);
|
|
}
|
|
|
|
get numberRows() {
|
|
return this.chunk(this.numbers, 6);
|
|
}
|
|
|
|
get numericPadEnabled() {
|
|
return this.selectedLabel !== null && (this.selectedNumbers.length > 0 || this.selectedNumbers.includes('F'));
|
|
}
|
|
|
|
get showShashEnter(): boolean {
|
|
const label = this.selectedLabel || '';
|
|
if (['FRP', 'QNP', 'TNP'].includes(label) && this.isBoxed) {
|
|
return false;
|
|
}
|
|
const specialLabels = ['FRP', 'QNP', 'TNP', 'EXA', 'TBP', 'MJP', 'JPP', '.'];
|
|
if (this.multiLegLabels.includes(label)) {
|
|
const maxLegs = this.getMaxLegs(this.currentPool || '');
|
|
return this.multiLegStage < maxLegs - 1;
|
|
}
|
|
return specialLabels.includes(label);
|
|
}
|
|
|
|
get isShashEnterDisabled(): boolean {
|
|
if (this.selectedLabel === 'TNP') {
|
|
if (this.isBoxed) return true;
|
|
return this.tanGroupStage >= 2 || this.tanGroups[this.tanGroupStage].length === 0;
|
|
} else if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
const maxLegs = this.getMaxLegs(this.currentPool || '');
|
|
return this.multiLegStage >= maxLegs || this.multiLegGroups[this.multiLegStage].length === 0;
|
|
} else if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
|
|
return this.isFirstGroupComplete || this.firstGroup.length === 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
get showBackspace(): boolean {
|
|
return this.selectedLabel !== null &&
|
|
(this.selectedNumbers.length > 0 || this.selectedNumbers.includes('F')) &&
|
|
this.padValue.length === 0;
|
|
}
|
|
|
|
get isBoxToggleDisabled(): boolean {
|
|
if (!this.selectedLabel) return true;
|
|
const disallowedBoxLabels = ['WNP', 'SHP', 'THP', 'PLP', 'WSP', 'SHW', 'TBP', 'MJP', 'JPP'];
|
|
if (disallowedBoxLabels.includes(this.selectedLabel)) return true;
|
|
if (this.selectedNumbers.includes('F')) return true;
|
|
if (['TNP', 'QNP', 'FRP'].includes(this.selectedLabel) && this.selectedNumbers.length > 0) return true;
|
|
return false;
|
|
}
|
|
|
|
get isWSPSelection(): boolean {
|
|
const currentRow = this.selectionService.getCurrentRow();
|
|
return ['WNP', 'SHP', 'PLP'].includes(currentRow.label) ||
|
|
this.selectionService.getSelections().some(sel => ['WNP', 'SHP', 'PLP'].includes(sel.label));
|
|
}
|
|
|
|
private chunk<T>(array: T[], size: number): T[][] {
|
|
return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
|
|
array.slice(i * size, i * size + size)
|
|
);
|
|
}
|
|
|
|
isLabelDisabled(label: string): boolean {
|
|
if (this.disabledLabels.includes(label) || this.totalAmountLimitReached || this.blockedLabels.has(label)) return true;
|
|
if (this.multiLegLabels.includes(label)) {
|
|
const poolKey = this.labelToPoolKey(label);
|
|
const raceIndices = this.getRaceIndicesForPoolExact(poolKey || '');
|
|
if (raceIndices.length === 0) return true;
|
|
if (raceIndices.some(raceNo => this.stopbetStatuses.get(raceNo) === 'Y')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private labelToPoolKey(label: string): string | null {
|
|
switch (label) {
|
|
case 'TBP': return this.findExistingPoolKey(['TBP1', 'TBP2', 'TRB1', 'TRB2']);
|
|
case 'MJP': return this.findExistingPoolKey(['MJP1', 'MJP2']);
|
|
case 'JPP': return this.findExistingPoolKey(['JPP1', 'JPP2', 'JKP1', 'JKP2']);
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
isNumberDisabled(number: number): boolean {
|
|
// Disable if number not present in actualRunners
|
|
if (!this.actualRunners.has(number)) return true;
|
|
// Disable if total amount limit reached
|
|
if (this.totalAmountLimitReached) return true;
|
|
// Allow all numbers for TNP when boxed, but disable selected numbers
|
|
if (this.selectedLabel === 'TNP' && this.isBoxed) {
|
|
return this.selectedNumbers.includes(number);
|
|
}
|
|
// TNP (unboxed): Disable numbers already selected in the current group
|
|
if (this.selectedLabel === 'TNP') {
|
|
return this.tanGroups[this.tanGroupStage].includes(number);
|
|
}
|
|
// Multi-leg pools (TRE, MJP, JKP): Disable numbers already selected in the current leg
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
return this.multiLegGroups[this.multiLegStage].includes(number);
|
|
}
|
|
// Two-group pools (FRP, QNP): Disable numbers already selected in the current group
|
|
if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
|
|
if (!this.isFirstGroupComplete) return this.firstGroup.includes(number);
|
|
return this.secondGroup.includes(number);
|
|
}
|
|
return this.selectedNumbers.includes(number);
|
|
}
|
|
|
|
selectLabel(label: string) {
|
|
if (this.totalAmountLimitReached || this.blockedLabels.has(label)) {
|
|
this.showLimitPopup = true;
|
|
return;
|
|
}
|
|
if (label === 'TBP') {
|
|
this.trePopupVisible = true;
|
|
return;
|
|
}
|
|
|
|
this.selectedLabel = label;
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.canPrint = false;
|
|
this.isBoxed = false;
|
|
|
|
// Store base race index and pool name for multi-leg pools
|
|
if (this.multiLegLabels.includes(label)) {
|
|
const poolKey = this.labelToPoolKey(label);
|
|
if (!poolKey) {
|
|
this.selectedLabel = null;
|
|
this.refreshBlockedLabels(null);
|
|
return;
|
|
}
|
|
this.currentPool = poolKey;
|
|
const baseRace = this.getBaseRaceIndexForPoolExact(poolKey);
|
|
if (!baseRace) {
|
|
this.selectedLabel = null;
|
|
this.refreshBlockedLabels(null);
|
|
return;
|
|
}
|
|
this.multiLegBaseRaceIdx = baseRace;
|
|
this.sharedStateService.updateSharedData({
|
|
type: 'multiLegPoolStart',
|
|
value: { label: poolKey, baseRaceIdx: this.multiLegBaseRaceIdx }
|
|
});
|
|
this.updateLegRaceDisplay(poolKey);
|
|
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
|
|
this.runnerCount = this.getRunnerCountForLegExact(this.getRaceForLegExact(poolKey, 0));
|
|
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
this.numbersFlat = this.numberRows.flat();
|
|
// Call after pool change
|
|
this.refreshBlockedLabels(label);
|
|
} else {
|
|
this.currentPool = null;
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.sharedStateService.updateSharedData({
|
|
type: 'multiLegPoolEnd',
|
|
value: null
|
|
});
|
|
// --- NEW: Update actualRunners for single race ---
|
|
this.setActualRunners();
|
|
// Call after pool change
|
|
this.refreshBlockedLabels(label);
|
|
}
|
|
|
|
//----------------------------------ADDED THIS -----------------------------------------------------
|
|
|
|
if (label === 'WSP') {
|
|
const wspLabels = ['WNP', 'SHP', 'PLP'];
|
|
this.wspTicketStage = 0;
|
|
this.selectionService.finalizeCurrentRow();
|
|
const currentSelections = this.selectionService.getSelections();
|
|
const existingWSP = currentSelections.filter(sel => wspLabels.includes(sel.label));
|
|
if (existingWSP.length === 0) {
|
|
const blankRows = wspLabels.map(lbl => ({
|
|
label: lbl,
|
|
numbers: [],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: false
|
|
}));
|
|
const totalExisting = currentSelections.reduce((sum, r) => sum + r.total, 0);
|
|
if (totalExisting <= 5000) {
|
|
this.selectionService.setSelections([...currentSelections, ...blankRows]);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(label);
|
|
}
|
|
}
|
|
this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0 });
|
|
// Call before return in WSP
|
|
this.refreshBlockedLabels(label);
|
|
return;
|
|
}
|
|
//----------------------------------ended here----------------------------------------------------
|
|
|
|
// Reset group states
|
|
this.tanGroupStage = 0;
|
|
this.tanGroups = [[], [], []];
|
|
// Reset FRP/QNP
|
|
this.isFirstGroupComplete = false;
|
|
this.firstGroup = [];
|
|
this.secondGroup = [];
|
|
// Reset Multi-leg
|
|
this.multiLegStage = 0;
|
|
this.multiLegGroups = [[], [], [], [], []];
|
|
|
|
this.selectionService.updatePartial({ label });
|
|
// recompute blocked labels including newly-selected label
|
|
this.refreshBlockedLabels(label);
|
|
}
|
|
|
|
selectNumber(number: number) {
|
|
if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return;
|
|
|
|
// TNP boxed
|
|
if (this.selectedLabel === 'TNP' && this.isBoxed) {
|
|
if (!this.selectedNumbers.includes(number)) {
|
|
const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[];
|
|
const allBoxed = [...currentNumbers, number];
|
|
const groupSize = Math.ceil(allBoxed.length / 3);
|
|
const group1 = allBoxed.slice(0, groupSize);
|
|
const group2 = allBoxed.slice(group1.length, group1.length + groupSize);
|
|
const group3 = allBoxed.slice(group1.length + group2.length);
|
|
const combined: (number | string)[] = [...group1];
|
|
if (group2.length) combined.push('-', ...group2);
|
|
if (group3.length) combined.push('-', ...group3);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({
|
|
numbers: [...this.selectedNumbers],
|
|
isBoxed: true,
|
|
label: 'TNP'
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// TNP unboxed
|
|
if (this.selectedLabel === 'TNP') {
|
|
if (!this.tanGroups[this.tanGroupStage].includes(number)) {
|
|
this.tanGroups[this.tanGroupStage].push(number);
|
|
const combined: (number | string)[] = [...this.tanGroups[0]];
|
|
if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]);
|
|
if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Multi-leg
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
if (!this.multiLegGroups[this.multiLegStage].includes(number)) {
|
|
this.multiLegGroups[this.multiLegStage].push(number);
|
|
this.updateMultiLegSelection();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
|
|
console.log('Selected label:', this.selectedLabel);
|
|
console.log('Current number clicked:', number);
|
|
|
|
if (!this.isFirstGroupComplete) {
|
|
console.log('First group not complete. Current firstGroup:', this.firstGroup);
|
|
if (!this.firstGroup.includes(number)) {
|
|
console.log(`Adding ${number} to firstGroup`);
|
|
this.firstGroup.push(number);
|
|
this.selectedNumbers = [...this.firstGroup];
|
|
} else {
|
|
console.log(`${number} already exists in firstGroup`);
|
|
}
|
|
} else {
|
|
console.log('First group complete. Moving to secondGroup. Current secondGroup:', this.secondGroup);
|
|
if (!this.secondGroup.includes(number)) {
|
|
console.log(`Adding ${number} to secondGroup`);
|
|
this.secondGroup.push(number);
|
|
this.selectedNumbers = [...this.firstGroup, '-', ...this.secondGroup];
|
|
} else {
|
|
console.log(`${number} already exists in secondGroup`);
|
|
}
|
|
}
|
|
|
|
console.log('Updated selectedNumbers:', this.selectedNumbers);
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
return;
|
|
}
|
|
|
|
// default single selection
|
|
if (!this.selectedNumbers.includes(number)) {
|
|
this.selectedNumbers.push(number);
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
// ✅ Special logic: If WSP, mirror number to WNP, SHP, and THP
|
|
|
|
if (this.selectedLabel === 'WSP') {
|
|
const labelsToUpdate = ['WNP', 'SHP', 'PLP'];
|
|
const selections = this.selectionService.getSelections();
|
|
const updated = selections.map(sel => {
|
|
if (labelsToUpdate.includes(sel.label)) {
|
|
const newNumbers = [...sel.numbers];
|
|
if (!newNumbers.includes(number)) newNumbers.push(number);
|
|
return { ...sel, numbers: newNumbers };
|
|
}
|
|
return sel;
|
|
});
|
|
this.selectionService.setSelections(updated);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
}
|
|
}
|
|
}
|
|
|
|
private updateMultiLegSelection() {
|
|
const combined: (number | string)[] = [];
|
|
for (let i = 0; i <= this.multiLegStage; i++) {
|
|
if (i > 0) combined.push('/');
|
|
combined.push(...this.multiLegGroups[i]);
|
|
}
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
this.updateLegRaceDisplay(this.currentPool || '');
|
|
}
|
|
|
|
private calculateMultiLegAmount(poolType: 'TBP' | 'MJP' | 'JPP', horsesPerLeg: number[], units: number, unitBet: number = 10): number {
|
|
return horsesPerLeg.reduce((acc, v) => acc * v, 1) * units * unitBet;
|
|
}
|
|
|
|
createVirtualRowsFromWSP(): SelectionData[] {
|
|
const base = this.selectionService.getCurrentRow();
|
|
if (!base.numbers.length || base.value <= 0 || ['WNP', 'SHP', 'PLP'].includes(base.label)) {
|
|
return [];
|
|
}
|
|
return ['WNP', 'SHP', 'PLP'].map(label => {
|
|
const newRow: SelectionData = {
|
|
...base,
|
|
label,
|
|
total: 0
|
|
};
|
|
newRow.total = this.calculateTotal(newRow);
|
|
return newRow;
|
|
});
|
|
}
|
|
|
|
private calculateTotal(row: SelectionData): number {
|
|
if (!row.numbers || row.numbers.length === 0 || row.value <= 0) {
|
|
return 0;
|
|
}
|
|
let combinations = 1;
|
|
if (['TNP', 'FRP', 'QNP'].includes(row.label)) {
|
|
combinations = row.numbers.length * (row.numbers.length - 1);
|
|
} else if (['TBP', 'JPP', 'MJP'].includes(row.label)) {
|
|
const legs = row.numbers.join('').split('/').map(leg => leg.split(',').length);
|
|
combinations = legs.reduce((a, b) => a * b, 1);
|
|
} else if (row.isBoxed) {
|
|
const n = row.numbers.length;
|
|
combinations = n > 1 ? (n * (n - 1)) / 2 : 0;
|
|
}
|
|
return combinations * row.value * 10;
|
|
}
|
|
|
|
clearWSPSelection() {
|
|
if (this.selectedLabel !== 'WSP') return;
|
|
|
|
const labels = ['WNP', 'SHP', 'PLP'];
|
|
const targetLabel = labels[this.wspTicketStage];
|
|
// Update only the current WSP stage's value to 0
|
|
const updatedSelections = this.selectionService.getSelections().map(sel => {
|
|
if (sel.label === targetLabel && JSON.stringify(sel.numbers) === JSON.stringify(this.selectedNumbers)) {
|
|
return { ...sel, value: 0, total: 0 };
|
|
}
|
|
return sel;
|
|
});
|
|
|
|
this.selectionService.setSelections(updatedSelections);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
this.padValue = '';
|
|
this.updateCanPrint();
|
|
}
|
|
|
|
onPadEnter() {
|
|
if (this.maxRowsReached) return;
|
|
|
|
if (!this.canPrint) {
|
|
this.print();
|
|
}
|
|
|
|
const value = parseFloat(this.padValue) || 0;
|
|
if (this.selectedLabel === 'WSP') {
|
|
const labels = ['WNP', 'SHP', 'PLP'];
|
|
const targetLabel = labels[this.wspTicketStage];
|
|
const selections = this.selectionService.getSelections();
|
|
// Find the current WSP row to ensure numbers are synchronized
|
|
|
|
const currentWSPRow = selections.find(sel => sel.label === targetLabel);
|
|
if (currentWSPRow) {
|
|
this.selectedNumbers = [...currentWSPRow.numbers];
|
|
}
|
|
|
|
const updatedSelections = selections.map(sel => {
|
|
if (sel.label === targetLabel) {
|
|
const total = value * (sel.numbers?.length || 0) * 10;
|
|
return { ...sel, value, total };
|
|
}
|
|
return sel;
|
|
});
|
|
|
|
this.selectionService.setSelections(updatedSelections);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
// Only increment stage if not at the last stage (PLP)
|
|
|
|
if (this.wspTicketStage < 2) {
|
|
this.wspTicketStage++;
|
|
// Update selectedNumbers for the next stage
|
|
const nextLabel = labels[this.wspTicketStage];
|
|
const nextWSPRow = updatedSelections.find(sel => sel.label === nextLabel);
|
|
if (nextWSPRow) this.selectedNumbers = [...nextWSPRow.numbers];
|
|
}
|
|
this.padValue = '';
|
|
this.updateCanPrint();
|
|
return;
|
|
}
|
|
|
|
this.print();
|
|
}
|
|
|
|
onShashEnter() {
|
|
if (this.selectedLabel === 'TNP' && this.isBoxed) return;
|
|
|
|
if (this.selectedLabel === 'TNP') {
|
|
if (this.tanGroupStage < 2 && this.tanGroups[this.tanGroupStage].length > 0) {
|
|
this.tanGroupStage++;
|
|
const combined: (number | string)[] = [...this.tanGroups[0]];
|
|
if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]);
|
|
if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
const maxLegs = this.getMaxLegs(this.currentPool || '');
|
|
if (this.multiLegStage < maxLegs - 1 && this.multiLegGroups[this.multiLegStage].length > 0) {
|
|
this.multiLegStage++;
|
|
this.updateMultiLegSelection();
|
|
this.updateLegRaceDisplay(this.currentPool || '');
|
|
// --- NEW: Update actualRunners for the new leg ---
|
|
this.setActualRunners();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
|
|
if (!this.isFirstGroupComplete && this.firstGroup.length > 0) {
|
|
this.isFirstGroupComplete = true;
|
|
this.secondGroup = [];
|
|
this.selectedNumbers = [...this.firstGroup, '-'];
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
}
|
|
}
|
|
}
|
|
//-----------------------------ENTER PAD VALUE-------------------------------------
|
|
|
|
enterPadVal(key: string) {
|
|
if (!this.numericPadEnabled || this.totalAmountLimitReached) return;
|
|
|
|
if (key === 'X') {
|
|
if (this.selectedLabel === 'WSP') {
|
|
this.clearWSPSelection();
|
|
return;
|
|
}
|
|
this.padValue = '';
|
|
this.selectionService.updatePartial({
|
|
label: this.selectedLabel || '',
|
|
numbers: [...this.selectedNumbers],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: this.isBoxed
|
|
});
|
|
this.updateCanPrint();
|
|
return;
|
|
}
|
|
|
|
if (/[0-9]/.test(key)) {
|
|
const currentValue = parseInt(this.padValue + key) || 0;
|
|
if (currentValue > 100) return;
|
|
this.padValue += key;
|
|
}
|
|
|
|
this.updateCanPrint();
|
|
|
|
const value = parseFloat(this.padValue) || 0;
|
|
|
|
if (this.selectedLabel === 'WSP') {
|
|
const labels = ['WNP', 'SHP', 'PLP'];
|
|
const targetLabel = labels[this.wspTicketStage];
|
|
const updatedSelections = this.selectionService.getSelections().map(sel => {
|
|
if (sel.label === targetLabel && JSON.stringify(sel.numbers) === JSON.stringify(this.selectedNumbers)) {
|
|
const total = value * (sel.numbers?.length || 0) * 10;
|
|
return { ...sel, value, total };
|
|
}
|
|
return sel;
|
|
});
|
|
|
|
this.selectionService.setSelections(updatedSelections);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
|
|
const currentTotal = updatedSelections.find(sel => sel.label === targetLabel)?.total || 0;
|
|
if (currentTotal === 0 && value > 0) {
|
|
console.log('[DEBUG] WSP row invalid (total = 0), clearing row');
|
|
this.selectionService.setSelections(
|
|
updatedSelections.filter(sel => sel.label !== targetLabel || sel.numbers.length > 0)
|
|
);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
this.wspTicketStage = 0;
|
|
this.padValue = '';
|
|
this.selectedNumbers = [];
|
|
this.selectedLabel = null;
|
|
this.selectionService.updatePartial({
|
|
label: '',
|
|
numbers: [],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: false
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
const maxLegs = this.getMaxLegs(this.currentPool || '');
|
|
const legsFilled = this.multiLegGroups.slice(0, maxLegs).every(group => group.length > 0);
|
|
|
|
if (!legsFilled) {
|
|
console.log('[DEBUG] Multi-leg pool incomplete (not all legs filled), clearing row');
|
|
this.selectionService.updatePartial({
|
|
label: '',
|
|
numbers: [],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: false
|
|
});
|
|
this.selectedLabel = null;
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.isBoxed = false;
|
|
this.multiLegStage = 0;
|
|
this.multiLegGroups = [[], [], [], [], []];
|
|
this.currentPool = null;
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.sharedStateService.updateSharedData({
|
|
type: 'multiLegPoolEnd',
|
|
value: null
|
|
});
|
|
this.updateCanPrint();
|
|
return;
|
|
}
|
|
|
|
this.selectionService.updatePartial({
|
|
value,
|
|
isBoxed: this.isBoxed,
|
|
label: this.selectedLabel || '',
|
|
numbers: [...this.selectedNumbers]
|
|
});
|
|
|
|
const currentRow = this.selectionService.getCurrentRow();
|
|
if (currentRow.total === 0 && value > 0 && currentRow.numbers.length > 0) {
|
|
console.log('[DEBUG] Multi-leg row invalid (total = 0), auto-clearing current row');
|
|
this.selectionService.updatePartial({
|
|
label: '',
|
|
numbers: [],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: false
|
|
});
|
|
this.selectedLabel = null;
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.isBoxed = false;
|
|
this.multiLegStage = 0;
|
|
this.multiLegGroups = [[], [], [], [], []];
|
|
this.currentPool = null;
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.sharedStateService.updateSharedData({
|
|
type: 'multiLegPoolEnd',
|
|
value: null
|
|
});
|
|
this.updateCanPrint();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.selectionService.updatePartial({
|
|
value,
|
|
isBoxed: this.isBoxed,
|
|
label: this.selectedLabel || '',
|
|
numbers: [...this.selectedNumbers]
|
|
});
|
|
|
|
const currentRow = this.selectionService.getCurrentRow();
|
|
if (currentRow.total === 0 && value > 0 && currentRow.numbers.length > 0) {
|
|
console.log('[DEBUG] Row invalid (total = 0), auto-clearing current row');
|
|
this.selectionService.updatePartial({
|
|
label: '',
|
|
numbers: [],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: false
|
|
});
|
|
this.selectedLabel = null;
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.isBoxed = false;
|
|
this.updateCanPrint();
|
|
}
|
|
}
|
|
|
|
updateCanPrint() {
|
|
if (this.maxRowsReached) { this.canPrint = false; return; }
|
|
this.canPrint = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue);
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
const maxLegs = this.getMaxLegs(this.currentPool || '');
|
|
this.canPrint = this.canPrint && this.multiLegStage === maxLegs - 1 && this.multiLegGroups[this.multiLegStage].length > 0;
|
|
}
|
|
}
|
|
|
|
// Add this getter for print button enable logic
|
|
get canPrintTicket(): boolean {
|
|
// At least one valid row in finalized selections or current row
|
|
const selections = this.selectionService.getSelections();
|
|
const currentRow = this.selectionService.getCurrentRow();
|
|
if (this.selectedLabel === 'WSP') {
|
|
// For WSP, require all three rows (WNP, SHP, PLP) to have valid numbers and values >= 1
|
|
const wspLabels = ['WNP', 'SHP', 'PLP'];
|
|
const wspSelections = selections.filter(sel => wspLabels.includes(sel.label));
|
|
const allWSPRowsValid = wspSelections.length === 3 && wspSelections.every(row =>
|
|
row.label && row.numbers && row.numbers.length > 0 && row.value >= 1 && row.total > 0
|
|
);
|
|
return this.wspTicketStage === 2 && allWSPRowsValid;
|
|
}
|
|
// For non-WSP, keep existing logic: any valid row enables printing
|
|
const hasValidRow = selections.some(
|
|
row => !!row.label && !!row.numbers && row.numbers.length > 0 && row.value > 0 && row.total > 0
|
|
) || (
|
|
!!currentRow.label && !!currentRow.numbers && currentRow.numbers.length > 0 && currentRow.value > 0 && currentRow.total > 0
|
|
);
|
|
return Boolean(hasValidRow);
|
|
}
|
|
|
|
print() {
|
|
const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + (sel.total || 0), 0);
|
|
let currentRowAmount = 0;
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
const maxLegs = this.getMaxLegs(this.currentPool || '');
|
|
const horsesPerLeg = this.multiLegGroups.map((group, index) => {
|
|
if (group.includes('F')) {
|
|
return this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, index);
|
|
}
|
|
return group.length;
|
|
}).slice(0, maxLegs);
|
|
const units = parseFloat(this.padValue) || 0;
|
|
currentRowAmount = this.calculateMultiLegAmount(this.selectedLabel as 'TBP' | 'MJP' | 'JPP', horsesPerLeg, units);
|
|
if (currentRowAmount > 5000 || selectionsTotal + currentRowAmount > 5000) {
|
|
this.totalAmountLimitReached = true;
|
|
this.showLimitPopup = true;
|
|
return;
|
|
}
|
|
// Ensure all legs have selections
|
|
if (horsesPerLeg.some(count => count === 0)) return;
|
|
} else {
|
|
currentRowAmount = this.currentTotal;
|
|
if (selectionsTotal + currentRowAmount > 5000) {
|
|
this.totalAmountLimitReached = true;
|
|
this.showLimitPopup = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.selectedLabel === 'WSP') {
|
|
const virtualRows = this.createVirtualRowsFromWSP();
|
|
const currentSelections = this.selectionService.getSelections();
|
|
const nonWSPSelections = currentSelections.filter(sel => !['WNP', 'SHP', 'PLP'].includes(sel.label));
|
|
this.selectionService.setSelections([...nonWSPSelections, ...virtualRows]);
|
|
// Call after setSelections
|
|
this.refreshBlockedLabels(this.selectedLabel);
|
|
}
|
|
|
|
this.selectionService.finalizeCurrentRow();
|
|
// Call after finalizeCurrentRow
|
|
this.refreshBlockedLabels(null);
|
|
this.resetSelections();
|
|
}
|
|
|
|
//-------------------PRINT LOGIC----------------------------------------
|
|
async printTicket() {
|
|
const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + (sel.total || 0), 0);
|
|
if (selectionsTotal + this.currentTotal > 5000) {
|
|
this.showLimitPopup = true;
|
|
this.cdr.markForCheck();
|
|
return;
|
|
}
|
|
console.log('[DEBUG] Horse numbers for selected race:', this.getHorseNumbersForSelectedRace());
|
|
console.log("🖨️ Print ticket clicked");
|
|
|
|
let selections = this.selectionService.getSelections();
|
|
const currentRow = this.selectionService.getCurrentRow();
|
|
|
|
if (this.selectedLabel === 'WSP') {
|
|
selections = selections.filter(sel => ['WNP', 'SHP', 'PLP'].includes(sel.label));
|
|
}
|
|
|
|
let allRows = [...selections];
|
|
|
|
let clickCount = Number(localStorage.getItem('printClickCount') || '0');
|
|
clickCount += 1;
|
|
localStorage.setItem('printClickCount', clickCount.toString());
|
|
|
|
if (currentRow.label && currentRow.numbers.length > 0 && currentRow.value > 0) {
|
|
if (!currentRow.total) {
|
|
let combinations = 1;
|
|
if (['TNP', 'FRP', 'QNP'].includes(currentRow.label)) {
|
|
combinations = currentRow.numbers.length * (currentRow.numbers.length - 1);
|
|
} else if (['TBP', 'JPP', 'MJP'].includes(currentRow.label)) {
|
|
combinations = 1;
|
|
} else if (currentRow.label === 'BOX') {
|
|
const n = currentRow.numbers.length;
|
|
combinations = n > 1 ? (n * (n - 1)) / 2 : 0;
|
|
}
|
|
currentRow.total = combinations * currentRow.value * 10;
|
|
}
|
|
allRows.push(currentRow);
|
|
}
|
|
|
|
if (allRows.length === 0) {
|
|
console.warn("No valid rows to print.");
|
|
this.cdr.markForCheck(); // <-- Ensure UI updates
|
|
return;
|
|
}
|
|
|
|
const ticketCount = allRows.reduce((sum, row) => sum + (row.value || 0), 0);
|
|
const totalAmount = allRows.reduce((sum, row) => sum + (row.total || 0), 0);
|
|
|
|
const now = new Date();
|
|
const venue = this.structuredRaceCard?.venue || '';
|
|
const day = String(now.getDate()).padStart(2, '0');
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
const year = String(now.getFullYear()).slice(-2);
|
|
const fullYear = now.getFullYear();
|
|
const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
|
const millis = now.getMilliseconds().toString().padStart(3, '0');
|
|
const btId = this.btid;
|
|
// const ticketId = `${venue}/${fullYear}${month}${day}/1`;
|
|
// For multi-leg pools (TRE, MJP, JKP), show the pool name (trb1, trb2, mjp1, jkp1) instead of race number
|
|
let ticketId: string;
|
|
if (['TBP', 'MJP', 'JPP'].includes(this.selectedLabel || '') && this.currentPool) {
|
|
ticketId = `${venue}/${fullYear}${month}${day}/${this.currentPool}`;
|
|
} else {
|
|
ticketId = `${venue}/${fullYear}${month}${day}/${this.selectedRaceNumber}`;
|
|
}
|
|
const barcodeId = `${btId}${day}${month}${year}${timeStr}${millis}`;
|
|
|
|
|
|
//----------------------------------------WINLABELS START HERE ------------------------------------------------------
|
|
const winLabels = allRows.map(row => {
|
|
let displayNumbers = row.numbers;
|
|
let displayLabel = row.label;
|
|
|
|
if (row.label === 'TBP' && this.currentPool) {
|
|
displayLabel = this.currentPool;
|
|
}
|
|
|
|
if (['TBP', 'MJP', 'JPP'].includes(row.label)) {
|
|
let legs: (number | string)[][] = [];
|
|
let currentLeg: (number | string)[] = [];
|
|
for (const n of displayNumbers) {
|
|
if (n === '/') {
|
|
legs.push(currentLeg);
|
|
currentLeg = [];
|
|
} else {
|
|
currentLeg.push(n);
|
|
}
|
|
}
|
|
if (currentLeg.length) legs.push(currentLeg);
|
|
|
|
const poolName = row.label === 'MJP' ? 'MJP1' : row.label === 'JPP' ? 'JPP1' : (this.currentPool || 'TBP1');
|
|
const poolKey = this.normalizePoolNameToKey(poolName) || poolName;
|
|
const raceIndices = this.getRaceIndicesForPoolExact(poolKey);
|
|
const baseRaceIdx = raceIndices.length > 0 ? raceIndices[0] : null;
|
|
|
|
const normalizeToken = (tok: string | number) => {
|
|
const s = String(tok);
|
|
return /^\d+$/.test(s) ? s.padStart(2, '0') : s;
|
|
};
|
|
|
|
const expandedLegs: string[] = legs.map((leg, i) => {
|
|
const raceNo = baseRaceIdx !== null ? (raceIndices.length > i ? raceIndices[i] : (baseRaceIdx + i)) : null;
|
|
const expanded = leg.flatMap((n) => {
|
|
if (n === 'F' && raceNo !== null) {
|
|
return this.getHorseNumbersForRaceIdx(raceNo).map(num => String(num));
|
|
}
|
|
return [n];
|
|
});
|
|
return expanded.filter(n => n !== '-' && n !== '#').map(normalizeToken).join(',');
|
|
});
|
|
|
|
const numbersStr = expandedLegs.join('/');
|
|
const labelForDisplay = row.label === 'TBP' ? 'TBP' : displayLabel;
|
|
const valueStr = `*${String(row.value || 0).padStart(3, '0')}`;
|
|
return `${labelForDisplay} ${numbersStr}${valueStr}`;
|
|
}
|
|
|
|
if (displayNumbers.includes('F')) {
|
|
displayNumbers = displayNumbers.flatMap((n) => {
|
|
if (n === 'F') {
|
|
return this.getHorseNumbersForSelectedRace().map(num => num.toString());
|
|
}
|
|
return [n];
|
|
});
|
|
}
|
|
|
|
let numbersStr = '';
|
|
if (['FRP', 'QNP'].includes(row.label)) {
|
|
const actualNumbers = displayNumbers.filter(n => n !== '#').join(',');
|
|
if (row.numbers.includes('#') || row.isBoxed) {
|
|
numbersStr = `${actualNumbers} - ${actualNumbers}`;
|
|
} else {
|
|
numbersStr = actualNumbers;
|
|
}
|
|
} else {
|
|
numbersStr = displayNumbers.filter(n => n !== '#').join(',');
|
|
}
|
|
|
|
const label = displayLabel.padEnd(10);
|
|
const numbers = numbersStr.padEnd(15);
|
|
const value = (`*${row.value || 0}`).padEnd(8);
|
|
const total = `Rs ${row.total || 0}`.padStart(8);
|
|
return `${label}${numbers} ${value} ${total}`;
|
|
}).join('\n');
|
|
|
|
// --- CONSOLE LOG FORMATTING ---
|
|
function formatNumbers(numStr: string) {
|
|
const legs = numStr.split(/\s*\/\s*/).map(leg => {
|
|
const parts = leg.split(',').map(p => p.trim()).filter(Boolean);
|
|
return parts.map(p => {
|
|
const numeric = p.match(/^\d+$/);
|
|
return numeric ? p.padStart(2, '0') : p;
|
|
}).join(',');
|
|
});
|
|
return legs.join('/');
|
|
}
|
|
|
|
// helper to pad ticket count to 3 digits (001..999)
|
|
function padTicketCountToThree(n: number) {
|
|
const num = Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
|
return String(num).padStart(3, '0');
|
|
}
|
|
|
|
function compactifyEntry(entry: string) {
|
|
const m = entry.match(/^(\S+)\s+(.+?)(\*\d{1,})?$/);
|
|
if (!m) return entry;
|
|
|
|
const label = m[1];
|
|
let numsPart = (m[2] || '').trim();
|
|
const starPart = m[3] || '';
|
|
|
|
numsPart = numsPart.replace(/\s+/g, ' ').trim();
|
|
|
|
const rawTokens = numsPart.split(',').map(s => s.trim());
|
|
const groups: string[][] = [];
|
|
let currentGroup: string[] = [];
|
|
|
|
for (const tok of rawTokens) {
|
|
if (!tok || tok === '#') continue;
|
|
if (tok === '-') {
|
|
groups.push(currentGroup);
|
|
currentGroup = [];
|
|
continue;
|
|
}
|
|
currentGroup.push(tok);
|
|
}
|
|
groups.push(currentGroup);
|
|
|
|
const normalizeToken = (tok: string) => {
|
|
const num = tok.match(/^\d+$/);
|
|
return num ? tok.padStart(2, '0') : tok;
|
|
};
|
|
|
|
const filteredGroups = groups
|
|
.map(g => g.filter(t => t && t !== '-' && t !== '#'))
|
|
.filter((g, idx) => !(g.length === 0 && groups.length === 1));
|
|
|
|
if (filteredGroups.length > 1) {
|
|
const groupStrs = filteredGroups.map(g => g.map(normalizeToken).join(','));
|
|
return `${label} ${groupStrs.join('-')}${starPart}`;
|
|
}
|
|
|
|
// 🔹 NEW RULE: check for "- -" style duplicate groups
|
|
if (filteredGroups.length === 2) {
|
|
const g1 = filteredGroups[0].map(normalizeToken);
|
|
const g2 = filteredGroups[1].map(normalizeToken);
|
|
if (g1.join(',') === g2.join(',')) {
|
|
return `${label} <<${g1.join(',')}>>${starPart}`;
|
|
}
|
|
}
|
|
|
|
const singleTokens = filteredGroups[0] || [];
|
|
if (singleTokens.length >= 2 && singleTokens.length % 2 === 0) {
|
|
const half = singleTokens.length / 2;
|
|
const firstHalf = singleTokens.slice(0, half).map(normalizeToken).join(',');
|
|
const secondHalf = singleTokens.slice(half).map(normalizeToken).join(',');
|
|
if (firstHalf === secondHalf) {
|
|
return `${label} ${firstHalf}-${secondHalf}${starPart}`;
|
|
}
|
|
}
|
|
|
|
const normalized = singleTokens.map(normalizeToken).join(',');
|
|
return `${label} ${normalized}${starPart}`;
|
|
}
|
|
|
|
const printData = {
|
|
ticketId,
|
|
barcodeId,
|
|
venue,
|
|
date: `${day}-${month}-${fullYear}`,
|
|
winLabels,
|
|
ticketCount,
|
|
totalAmount,
|
|
gstNumber: '29ABCDE1234F2Z5'
|
|
};
|
|
|
|
console.log('--- Simulated Ticket Print ---');
|
|
console.log(`Ticket ID : ${printData.ticketId}`);
|
|
console.log(`Barcode ID : ${printData.barcodeId}`);
|
|
console.log(`|||||| ||| | ||||| |||| |`);
|
|
console.log(`WIN Labels :`);
|
|
|
|
let formattedEntries: string[] = [];
|
|
printData.winLabels.split('\n').forEach(line => {
|
|
const m = line.match(/^(\S+)\s+(.+?)\s+\*(\d+)\b/);
|
|
if (m) {
|
|
const label = m[1];
|
|
const rawNums = m[2].trim();
|
|
const value = Number(m[3]) || 0;
|
|
const formattedNums = formatNumbers(rawNums);
|
|
const valueStr = `*${padTicketCountToThree(value)}`;
|
|
formattedEntries.push(`${label} ${formattedNums}${valueStr}`);
|
|
} else {
|
|
if (line.trim()) formattedEntries.push(line.trim());
|
|
}
|
|
});
|
|
|
|
let formattedWinLabels = formattedEntries.map(compactifyEntry).join('');
|
|
if (!formattedWinLabels) {
|
|
formattedWinLabels = printData.winLabels || '(no win labels)';
|
|
}
|
|
|
|
const formattedTotal = `₹${printData.totalAmount}`;
|
|
console.log(formattedWinLabels);
|
|
console.log(formattedTotal);
|
|
console.log(`GST Number : ${printData.gstNumber}`);
|
|
console.log(`Date/Time : ${now.toLocaleString()}`);
|
|
console.log('-----------------------------');
|
|
|
|
// --- BACKEND COMMIT ---
|
|
const ticketParts = (printData.ticketId || '').split('/');
|
|
const raceVenue = ticketParts[0] || 'MYS';
|
|
const dateStr = ticketParts[1] || '';
|
|
const raceDt = dateStr && dateStr.length === 8
|
|
? `${dateStr.slice(0,4)}/${dateStr.slice(4,6)}/${dateStr.slice(6,8)}`
|
|
: (new Date()).toISOString().slice(0,10).replace(/-/g,'/');
|
|
const raceNumRaw = (ticketParts[2] || '').trim();
|
|
let raceNum = '01';
|
|
|
|
const sufMatch = raceNumRaw.match(/^([a-zA-Z]+)(\d+)$/);
|
|
if (sufMatch) {
|
|
const prefix = sufMatch[1].toLowerCase();
|
|
const num = sufMatch[2];
|
|
const prefixMap: Record<string, string> = {
|
|
jkp: 'J',
|
|
mjp: 'M',
|
|
trb: 'T',
|
|
tbp: 'T'
|
|
};
|
|
const letter = prefixMap[prefix] ?? prefix.charAt(0).toUpperCase();
|
|
raceNum = `${letter}${num}`;
|
|
} else {
|
|
const asNum = Number(raceNumRaw);
|
|
if (Number.isFinite(asNum) && raceNumRaw !== '') {
|
|
raceNum = String(Math.floor(asNum)).padStart(2, '0');
|
|
}
|
|
}
|
|
|
|
const btId_bc = (printData.barcodeId || '0000').toString().slice(0,4);
|
|
const betInfo = formattedWinLabels || printData.winLabels || '';
|
|
const nHorseBits = [0,0,0,0,0,0,0,0];
|
|
const tktVal = Number(printData.totalAmount) || 0;
|
|
|
|
const commitPayload = {
|
|
btMake: "I",
|
|
btId: btId_bc,
|
|
raceVenue: raceVenue,
|
|
raceDt: raceDt,
|
|
raceNum: raceNum,
|
|
betInfo: betInfo,
|
|
nHorseBits: nHorseBits,
|
|
moneyTyp: "C",
|
|
tktVal: tktVal,
|
|
repeatFlag: "N"
|
|
};
|
|
|
|
console.log('➡️ Sending commit to /isr/commit:', commitPayload);
|
|
|
|
let commitJson = null;
|
|
try {
|
|
const commitResp = await fetch('http://localhost:8084/isr/commit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(commitPayload)
|
|
});
|
|
|
|
const txt = await commitResp.text();
|
|
try {
|
|
commitJson = txt ? JSON.parse(txt) : null;
|
|
} catch (e) {
|
|
console.warn('⚠️ /isr/commit returned non-JSON response:', txt);
|
|
commitJson = null;
|
|
}
|
|
|
|
if (!commitResp.ok) {
|
|
console.error('❌ /isr/commit HTTP error', commitResp.status, commitJson ?? txt);
|
|
this.cdr.markForCheck();
|
|
return;
|
|
}
|
|
if (!(commitJson && commitJson.success === true)) {
|
|
console.error('❌ /isr/commit failed or returned success=false:', commitJson);
|
|
if (commitJson && commitJson.message) {
|
|
console.error('Server message:', commitJson.message);
|
|
}
|
|
this.cdr.markForCheck();
|
|
return;
|
|
}
|
|
console.log('✅ /isr/commit success:', commitJson);
|
|
try {
|
|
localStorage.setItem('issueT', JSON.stringify(commitJson));
|
|
console.log('Saved commitJson into localStorage key: issueT');
|
|
localStorage.setItem('cancelT', JSON.stringify(commitJson));
|
|
} catch (e) {
|
|
console.warn('Failed to save issueT:', e);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ Print failed:", error);
|
|
this.cdr.markForCheck();
|
|
return;
|
|
}
|
|
|
|
let issueT: any = null;
|
|
try {
|
|
const raw = localStorage.getItem('issueT');
|
|
issueT = raw ? JSON.parse(raw) : null;
|
|
} catch (e) {
|
|
console.warn('Could not parse issueT from localStorage:', e);
|
|
}
|
|
|
|
const barcodeIdToUse = issueT?.tktNumUsed ?? printData.barcodeId;
|
|
|
|
const payload = {
|
|
type: 'ticket',
|
|
ticketId: printData.ticketId,
|
|
barcodeId: barcodeIdToUse,
|
|
winLabels: printData.winLabels,
|
|
ticketCount: printData.ticketCount,
|
|
totalAmount: printData.totalAmount,
|
|
gstNumber: printData.gstNumber,
|
|
dateTime: now.toLocaleString()
|
|
};
|
|
|
|
console.log('Printer payload:', payload);
|
|
|
|
|
|
fetch('http://localhost:9100/print', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
// ---------------------sending data to backend ---------------------------------
|
|
|
|
try {
|
|
const existingTicketsStr = localStorage.getItem('localTickets');
|
|
const existingTickets = existingTicketsStr ? JSON.parse(existingTicketsStr) : [];
|
|
existingTickets.push(payload);
|
|
localStorage.setItem('localTickets', JSON.stringify(existingTickets));
|
|
console.log('📦 [DEBUG] Ticket saved locally to localStorage.');
|
|
} catch (error) {
|
|
console.error('❌ Failed to store ticket locally:', error);
|
|
}
|
|
|
|
try {
|
|
const existingTicketsStr = localStorage.getItem('localTicketsViewlog');
|
|
const existingTickets = existingTicketsStr ? JSON.parse(existingTicketsStr) : [];
|
|
if (existingTickets.length >= 10) {
|
|
existingTickets.shift();
|
|
}
|
|
existingTickets.push(payload);
|
|
localStorage.setItem('localTicketsViewlog', JSON.stringify(existingTickets));
|
|
console.log('📦 [DEBUG] Ticket saved locally to localStorage.');
|
|
} catch (error) {
|
|
console.error('❌ Failed to store ticket locally:', error);
|
|
}
|
|
|
|
//--------------------------------ENDED HERE ------------------------------------------------
|
|
|
|
try {
|
|
localStorage.setItem('localTicketsnew', JSON.stringify([payload]));
|
|
console.log('📦 [DEBUG] Latest ticket stored in localStorage (previous cleared).');
|
|
localStorage.setItem('canceltickets', JSON.stringify([payload]));
|
|
} catch (error) {
|
|
console.error('❌ Failed to store ticket locally:', error);
|
|
}
|
|
|
|
fetch('http://192.168.1.12:8083/api/tickets', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
//----------------------------------------ends here --------------------------
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Printer error: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(result => {
|
|
console.log("✅ Print successful:", result);
|
|
this.erase();
|
|
})
|
|
.catch(error => {
|
|
console.error("❌ Print failed:", error);
|
|
this.erase();
|
|
});
|
|
|
|
this.erase();
|
|
}
|
|
|
|
clearLocalTickets() {
|
|
localStorage.removeItem('localTickets');
|
|
console.log('🧹 localTickets cleared from localStorage');
|
|
localStorage.removeItem('printClickCount');
|
|
console.log('🧼 printClickCount cleared from localStorage');
|
|
this.sharedStateService.setSalesTotal(0);
|
|
this.sharedStateService.setReceiveTotal(0);
|
|
this.selectionService.clearSelections();
|
|
this.resetSelections();
|
|
}
|
|
|
|
//----------------------------------PRINT ENDS HERE ------------------------------------------------------
|
|
|
|
|
|
erase() {
|
|
this.selectionService.clearSelections();
|
|
this.resetSelections();
|
|
this.refreshBlockedLabels(null);
|
|
this.cdr.markForCheck();
|
|
}
|
|
|
|
resetSelections() {
|
|
this.selectedLabel = null;
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.canPrint = false;
|
|
this.isBoxed = false;
|
|
this.totalAmountLimitReached = false;
|
|
this.showLimitPopup = false;
|
|
this.tanGroupStage = 0;
|
|
this.tanGroups = [[], [], []];
|
|
this.isFirstGroupComplete = false;
|
|
this.firstGroup = [];
|
|
this.secondGroup = [];
|
|
this.multiLegStage = 0;
|
|
this.multiLegGroups = [[], [], [], [], []];
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.currentPool = null;
|
|
this.fieldModalOpen = false;
|
|
this.fieldInput = '';
|
|
this.fieldFEntered = false;
|
|
this.wspTicketStage = 0;
|
|
// Explicitly reset blocked labels (no current label)
|
|
this.refreshBlockedLabels(null);
|
|
this.updateCanPrint();
|
|
this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null });
|
|
this.cdr.markForCheck();
|
|
}
|
|
|
|
toggleBoxMode() {
|
|
if (this.totalAmountLimitReached) return;
|
|
this.isBoxed = !this.isBoxed;
|
|
const value = parseFloat(this.padValue) || 0;
|
|
if (this.selectedLabel === 'TNP' && this.isBoxed) {
|
|
this.tanGroupStage = 0;
|
|
this.tanGroups = [[], [], []];
|
|
this.selectedNumbers = [];
|
|
}
|
|
this.selectionService.updatePartial({
|
|
isBoxed: this.isBoxed,
|
|
label: this.selectedLabel || '',
|
|
numbers: [...this.selectedNumbers],
|
|
value
|
|
});
|
|
this.updateCanPrint();
|
|
}
|
|
|
|
removeLastNumber() {
|
|
if (!this.selectedLabel || (this.selectedNumbers.length === 0 && !this.selectedNumbers.includes('F'))) return;
|
|
|
|
if (this.selectedNumbers.includes('F') && this.allowedFieldLabels.includes(this.selectedLabel || '')) {
|
|
this.selectedNumbers = [];
|
|
this.selectionService.updatePartial({ numbers: [], isBoxed: false, label: this.selectedLabel || '' });
|
|
return;
|
|
}
|
|
|
|
if (this.selectedLabel === 'TNP' && this.isBoxed) {
|
|
const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[];
|
|
if (currentNumbers.length > 0) {
|
|
currentNumbers.pop();
|
|
const groupSize = Math.ceil(currentNumbers.length / 3);
|
|
const group1 = currentNumbers.slice(0, groupSize);
|
|
const group2 = currentNumbers.slice(group1.length, group1.length + groupSize);
|
|
const group3 = currentNumbers.slice(group1.length + group2.length);
|
|
const combined: (number | string)[] = [...group1];
|
|
if (group2.length) combined.push('-', ...group2);
|
|
if (group3.length) combined.push('-', ...group3);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers], isBoxed: true, label: 'TNP' });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.selectedLabel === 'TNP') {
|
|
const currentGroup = this.tanGroups[this.tanGroupStage];
|
|
if (currentGroup.length > 0) {
|
|
currentGroup.pop();
|
|
let combined: (number | string)[] = [...this.tanGroups[0]];
|
|
if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]);
|
|
if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.multiLegLabels.includes(this.selectedLabel)) {
|
|
const currentGroup = this.multiLegGroups[this.multiLegStage];
|
|
if (currentGroup.length > 0) {
|
|
currentGroup.pop();
|
|
this.updateMultiLegSelection();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.twoGroupLabels.includes(this.selectedLabel)) {
|
|
if (!this.isFirstGroupComplete && this.firstGroup.length > 0) {
|
|
this.firstGroup.pop();
|
|
this.selectedNumbers = [...this.firstGroup];
|
|
} else if (this.secondGroup.length > 0) {
|
|
this.secondGroup.pop();
|
|
this.selectedNumbers = [...this.firstGroup, '-', ...this.secondGroup];
|
|
}
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
return;
|
|
}
|
|
|
|
this.selectedNumbers.pop();
|
|
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
|
|
}
|
|
|
|
private getMaxLegs(poolKey: string): number {
|
|
const key = this.normalizePoolNameToKey(poolKey) || poolKey;
|
|
const indices = this.getRaceIndicesForPoolExact(key);
|
|
return indices.length;
|
|
}
|
|
|
|
private getRaceForLeg(poolName: string, leg: number): number {
|
|
const key = this.normalizePoolNameToKey(poolName) || poolName;
|
|
const rn = this.getRaceForLegExact(key, leg);
|
|
return rn ?? 0;
|
|
}
|
|
|
|
private updateLegRaceDisplay(poolName: string) {
|
|
const poolKey = this.normalizePoolNameToKey(poolName) || poolName;
|
|
const raceNo = this.getRaceForLegExact(poolKey, this.multiLegStage);
|
|
if (!raceNo) {
|
|
this.currentLegRaceDisplay = '';
|
|
return;
|
|
}
|
|
this.currentLegRaceDisplay = `Leg ${this.multiLegStage + 1} (Race ${raceNo})`;
|
|
const runnerCount = this.getRunnerCountForLegExact(raceNo);
|
|
if (runnerCount) this.sharedStateService.setRunnerCount(runnerCount);
|
|
this.sharedStateService.updateSharedData({ type: 'currentLegRace', value: raceNo });
|
|
}
|
|
|
|
private getRunnerCountForLeg(baseIdx: number, leg: number): number {
|
|
if (!this.currentPool) return 0;
|
|
const poolKey = this.normalizePoolNameToKey(this.currentPool) || this.currentPool;
|
|
const raceNo = this.getRaceForLegExact(poolKey, leg);
|
|
return this.getRunnerCountForLegExact(raceNo);
|
|
}
|
|
|
|
private handleFieldForSpecialLabels() {
|
|
if (!this.selectedLabel) return;
|
|
|
|
if (this.selectedLabel === 'FRP' || this.selectedLabel === 'QNP') {
|
|
if (!this.isFirstGroupComplete) {
|
|
this.firstGroup = ['F'];
|
|
this.selectedNumbers = ['F'];
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
} else {
|
|
this.secondGroup = ['F'];
|
|
this.selectedNumbers = [...this.firstGroup, '-', 'F'];
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
}
|
|
} else if (this.selectedLabel === 'TNP') {
|
|
if (this.isBoxed) {
|
|
this.selectedNumbers = ['F'];
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true });
|
|
} else {
|
|
this.tanGroups[this.tanGroupStage] = ['F'];
|
|
const combined: (number | string)[] = [...this.tanGroups[0]];
|
|
if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]);
|
|
if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
}
|
|
} else if (this.multiLegLabels.includes(this.selectedLabel)) {
|
|
this.multiLegGroups[this.multiLegStage] = ['F'];
|
|
this.updateMultiLegSelection();
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
}
|
|
}
|
|
|
|
openCalculator() { this.calculatorOpen = true; this.calcDisplay = ''; }
|
|
closeCalculator() { this.calculatorOpen = false; }
|
|
press(val: string) { if (this.calcDisplay === 'Error') this.calcDisplay = ''; this.calcDisplay += val; }
|
|
clearDisplay() { this.calcDisplay = ''; }
|
|
backspace() { this.calcDisplay = this.calcDisplay === 'Error' ? '' : this.calcDisplay.slice(0, -1); }
|
|
calculate() {
|
|
try { this.calcDisplay = eval(this.calcDisplay).toString(); }
|
|
catch { this.calcDisplay = 'Error'; }
|
|
}
|
|
|
|
canUseField(): boolean {
|
|
if (this.totalAmountLimitReached) return false;
|
|
if (this.selectedLabel === 'FRP' || this.selectedLabel === 'QNP') {
|
|
if (!this.isFirstGroupComplete && this.firstGroup.length === 0) return true;
|
|
if (this.isFirstGroupComplete && this.secondGroup.length === 0) return true;
|
|
return false;
|
|
}
|
|
if (this.selectedLabel === 'TNP' || this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
if (this.selectedLabel === 'TNP' && this.tanGroups[this.tanGroupStage].length === 0) return true;
|
|
if (this.multiLegLabels.includes(this.selectedLabel || '') && this.multiLegGroups[this.multiLegStage].length === 0) return true;
|
|
return false;
|
|
}
|
|
return this.selectedLabel !== null && this.allowedFieldLabels.includes(this.selectedLabel) && this.selectedNumbers.length === 0;
|
|
}
|
|
|
|
openFieldModal() {
|
|
if (this.totalAmountLimitReached) return;
|
|
if (['FRP', 'QNP', 'TNP'].includes(this.selectedLabel || '') || this.multiLegLabels.includes(this.selectedLabel || '')) {
|
|
this.handleFieldForSpecialLabels();
|
|
return;
|
|
}
|
|
this.fieldModalOpen = true;
|
|
this.fieldInput = '';
|
|
this.fieldFEntered = false;
|
|
}
|
|
|
|
closeFieldModal() { this.fieldModalOpen = false; }
|
|
|
|
handleFieldKey(key: string) {
|
|
if (key === 'BACK') {
|
|
this.fieldInput = this.fieldInput.slice(0, -1);
|
|
if (!this.fieldInput.includes('F')) this.fieldFEntered = false;
|
|
return;
|
|
}
|
|
if (key === 'F') {
|
|
if (!this.fieldFEntered) {
|
|
this.fieldFEntered = true;
|
|
this.fieldInput = 'F';
|
|
}
|
|
return;
|
|
}
|
|
if (/[0-9]/.test(key)) {
|
|
this.fieldInput += key;
|
|
}
|
|
}
|
|
|
|
confirmFieldEntry() {
|
|
if (!this.selectedLabel) return;
|
|
|
|
if (this.selectedLabel === 'FRP' || this.selectedLabel === 'QNP') {
|
|
if (!this.isFirstGroupComplete) {
|
|
this.firstGroup = ['F'];
|
|
this.selectedNumbers = ['F'];
|
|
} else {
|
|
this.secondGroup = ['F'];
|
|
this.selectedNumbers = [...this.firstGroup, '-', 'F'];
|
|
}
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
this.closeFieldModal();
|
|
return;
|
|
}
|
|
|
|
if (this.selectedLabel === 'TNP') {
|
|
if (this.isBoxed) {
|
|
this.selectedNumbers = ['F'];
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true });
|
|
} else {
|
|
this.tanGroups[this.tanGroupStage] = ['F'];
|
|
const combined: (number | string)[] = [...this.tanGroups[0]];
|
|
if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]);
|
|
if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]);
|
|
this.selectedNumbers = combined;
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
}
|
|
this.closeFieldModal();
|
|
return;
|
|
}
|
|
|
|
if (this.multiLegLabels.includes(this.selectedLabel)) {
|
|
this.multiLegGroups[this.multiLegStage] = ['F'];
|
|
this.updateMultiLegSelection();
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
this.closeFieldModal();
|
|
return;
|
|
}
|
|
|
|
if (this.fieldFEntered) {
|
|
this.selectedNumbers = ['F'];
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: false });
|
|
this.closeFieldModal();
|
|
return;
|
|
}
|
|
|
|
const num = Number(this.fieldInput);
|
|
if (!Number.isFinite(num)) {
|
|
console.warn('[FIELD] invalid numeric entry:', this.fieldInput);
|
|
return;
|
|
}
|
|
|
|
if (!this.actualRunners.has(num)) {
|
|
console.warn('[FIELD] number not available on touch-pad for this race/leg:', num);
|
|
return;
|
|
}
|
|
|
|
this.selectedNumbers = [num];
|
|
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
|
|
this.closeFieldModal();
|
|
}
|
|
|
|
openPoolReplaceModal() {
|
|
if (this.totalAmountLimitReached) return;
|
|
const groupA = ['WNP', 'SHP', 'THP', 'PLP', 'SHW'];
|
|
const groupB = ['FRP', 'QNP', 'TNP'];
|
|
if (groupA.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupA;
|
|
else if (groupB.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupB;
|
|
else this.poolReplaceOptions = [];
|
|
this.poolReplaceOpen = true;
|
|
}
|
|
|
|
handlePoolReplace(label: string) {
|
|
this.selectedLabel = label;
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.canPrint = false;
|
|
this.isBoxed = false;
|
|
this.tanGroupStage = 0;
|
|
this.tanGroups = [[], [], []];
|
|
this.isFirstGroupComplete = false;
|
|
this.firstGroup = [];
|
|
this.secondGroup = [];
|
|
this.multiLegStage = 0;
|
|
this.multiLegGroups = [[], [], [], [], []];
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.currentPool = null;
|
|
this.fieldModalOpen = false;
|
|
this.fieldInput = '';
|
|
this.fieldFEntered = false;
|
|
this.wspTicketStage = 0;
|
|
this.poolReplaceOpen = false;
|
|
this.selectionService.updatePartial({ label });
|
|
this.refreshBlockedLabels(label);
|
|
}
|
|
|
|
closePoolReplaceModal() { this.poolReplaceOpen = false; }
|
|
|
|
treButtonClick(btnNum: number) {
|
|
this.trePopupVisible = false;
|
|
this._selectTreAfterPopup(btnNum);
|
|
}
|
|
|
|
private _selectTreAfterPopup(btnNum: number) {
|
|
// Initialize default state
|
|
this.selectedLabel = 'TBP';
|
|
this.selectedNumbers = [];
|
|
this.padValue = '';
|
|
this.canPrint = false;
|
|
this.isBoxed = false;
|
|
this.tanGroupStage = 0;
|
|
this.tanGroups = [[], [], []];
|
|
this.isFirstGroupComplete = false;
|
|
this.firstGroup = [];
|
|
this.secondGroup = [];
|
|
this.multiLegStage = 0;
|
|
this.multiLegGroups = [[], [], [], [], []];
|
|
|
|
// Map button to pool and normalize
|
|
const poolInfo = btnNum === 2 ? { name: 'trb2' } : { name: 'trb1' };
|
|
const poolKey = this.normalizePoolNameToKey(poolInfo.name);
|
|
|
|
// Handle invalid pool key
|
|
if (!poolKey) {
|
|
console.warn(`[DEBUG] Invalid pool key for ${poolInfo.name}`);
|
|
this.selectedLabel = null;
|
|
this.currentPool = null;
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0, isBoxed: false });
|
|
this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null });
|
|
this.refreshBlockedLabels(null);
|
|
this.cdr.markForCheck();
|
|
return;
|
|
}
|
|
|
|
// Get base race index and handle null case
|
|
const baseRaceIdx = this.getBaseRaceIndexForPoolExact(poolKey);
|
|
if (baseRaceIdx === null) {
|
|
console.warn(`[DEBUG] No base race index found for pool ${poolKey}`);
|
|
this.selectedLabel = null;
|
|
this.currentPool = null;
|
|
this.multiLegBaseRaceIdx = 0;
|
|
this.currentLegRaceDisplay = '';
|
|
this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0, isBoxed: false });
|
|
this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null });
|
|
this.refreshBlockedLabels(null);
|
|
this.cdr.markForCheck();
|
|
return;
|
|
}
|
|
|
|
// Set up multi-leg pool
|
|
this.currentPool = poolKey;
|
|
this.multiLegBaseRaceIdx = baseRaceIdx;
|
|
|
|
// Notify shared state of pool start
|
|
this.sharedStateService.updateSharedData({
|
|
type: 'multiLegPoolStart',
|
|
value: { label: poolKey, baseRaceIdx: this.multiLegBaseRaceIdx }
|
|
});
|
|
|
|
// Update UI and state
|
|
this.updateLegRaceDisplay(poolKey);
|
|
this.selectionService.updatePartial({ label: 'TBP' });
|
|
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
|
|
this.runnerCount = this.getRunnerCountForLegExact(this.getRaceForLegExact(poolKey, 0)) || 12;
|
|
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
this.numbersFlat = this.numberRows.flat();
|
|
this.refreshBlockedLabels('TBP');
|
|
this.cdr.markForCheck();
|
|
}
|
|
|
|
closeTrePopup() { this.trePopupVisible = false; }
|
|
closeLimitPopup() { this.showLimitPopup = false; }
|
|
|
|
copyNumbers() {
|
|
if (!this.selectedLabel) { console.warn('Please select a label before copying numbers.'); return; }
|
|
const selections = this.selectionService.getSelections();
|
|
if (selections.length === 0) { console.warn('No previous rows to copy from.'); return; }
|
|
|
|
// Copy numbers from the most recent finalized row
|
|
const lastRow = selections[selections.length - 1];
|
|
const numbersToCopy = [...lastRow.numbers];
|
|
// Apply the copied numbers directly
|
|
this.selectedNumbers = numbersToCopy;
|
|
|
|
// Validate against actual runners
|
|
const validNumbers = this.selectedNumbers.filter(num => {
|
|
if (typeof num === 'string') return num === 'F' || num === '-';
|
|
return this.actualRunners.has(num as number);
|
|
});
|
|
|
|
this.selectedNumbers = validNumbers;
|
|
// Update the current row in the selection service
|
|
this.selectionService.updatePartial({
|
|
label: this.selectedLabel,
|
|
numbers: [...this.selectedNumbers],
|
|
isBoxed: this.isBoxed,
|
|
value: parseFloat(this.padValue) || 0
|
|
});
|
|
|
|
this.updateCanPrint();
|
|
}
|
|
|
|
// Add trackByHorse for use in *ngFor
|
|
trackByHorse(index: number, item: number): number { return item; }
|
|
|
|
// Example usage of _.uniq for enabledHorseNumbers (if you ever set it)
|
|
setEnabledHorseNumbers(numbers: number[]) {
|
|
this.enabledHorseNumbers = _.uniq(numbers);
|
|
}
|
|
|
|
// If you ever need to deduplicate numbers before rendering:
|
|
get dedupedEnabledHorseNumbers(): number[] {
|
|
return _.uniq(this.enabledHorseNumbers);
|
|
}
|
|
// Update canEnterRow to require value between 1 and 100
|
|
get canEnterRow(): boolean {
|
|
if (this.maxRowsReached) return false;
|
|
if (this.selectedLabel === 'WSP') {
|
|
const isValidPadValue = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue);
|
|
const hasNumbers = this.selectedNumbers.length > 0;
|
|
return isValidPadValue && hasNumbers;
|
|
}
|
|
// Default logic for non-WSP
|
|
const currentRow = this.selectionService.getCurrentRow();
|
|
return !!currentRow.label &&
|
|
!!currentRow.numbers &&
|
|
currentRow.numbers.length > 0 &&
|
|
typeof currentRow.value === 'number' &&
|
|
currentRow.value >= 1 &&
|
|
currentRow.value <= 100 &&
|
|
currentRow.total > 0;
|
|
}
|
|
|
|
// add this in the component class (near other helpers)
|
|
private refreshBlockedLabels(currentLabel?: string | null) {
|
|
// Pass finalized selections and optionally the in-progress/current label
|
|
const finalized = this.selectionService.getSelections();
|
|
this.blockedLabels = this.labelRestrictionService.getBlockedLabels(finalized, currentLabel ?? this.selectedLabel);
|
|
console.log('[DEBUG] refreshBlockedLabels -> selectedLabel:', currentLabel ?? this.selectedLabel, 'blocked:', Array.from(this.blockedLabels));
|
|
// Ensure OnPush UI updates
|
|
try { this.cdr.markForCheck(); } catch (e) { /* ignore */ }
|
|
}
|
|
} |