btc_horse/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts

1837 lines
61 KiB
TypeScript
Executable File

import {
Component,
Input,
OnInit,
OnDestroy,
NgZone,
ChangeDetectionStrategy
} 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 _ 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 // 🔥 CRUCIAL for performance
})
export class TouchPadMenuComponent implements OnInit, OnDestroy {
@Input() ticketingActive: boolean = false;
public twoGroupLabels = ['FOR', 'QUI'];
public multiLegLabels = ['TRE', 'MJP', 'JKP'];
public threeGroupLabels = ['TAN'];
public allowedFieldLabels = ['WIN', 'SHP', 'THP', 'PLC', 'SHW'];
labels: string[] = [
'WIN', 'SHP', 'THP', 'PLC', 'SHW', 'FOR',
'QUI', 'TAN', 'EXA', 'WSP', 'TRE', 'MJP',
'JKP', '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', '.'];
// TAN logic
tanGroupStage = 0;
tanGroups: (number | string)[][] = [[], [], []];
// FOR/QUI logic
isFirstGroupComplete = false;
firstGroup: (number | string)[] = [];
secondGroup: (number | string)[] = [];
// Multi-leg logic (TRE, MJP, JKP)
multiLegStage = 0;
multiLegGroups: (number | string)[][] = [[], [], [], [], []];
multiLegBaseRaceIdx: number = 0; // Track starting race index
currentLegRaceDisplay: string = ''; // Display current leg's race
currentPool: string | null = null; // Track current pool (mjp1, jkp1, trb1, trb2)
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 currentTotal: number = 0;
private currentSelections: SelectionData[] = [];
enabledHorseNumbers: number[] = [];
prevEnabledKey: string = '';
raceCardData: any = {};
constructor(
private selectionService: SelectionService,
private sharedStateService: SharedStateService,
private labelRestrictionService: LabelRestrictionService,
private ngZone: NgZone // <-- inject NgZone
) {}
selectedRaceNumber: string = '1'; // Default
ngOnInit() {
this.runnerCountSubscription = this.sharedStateService.runnerCount$.subscribe(count => {
this.runnerCount = count || 12;
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
this.numbersFlat = this.numberRows.flat();
this.updateLegRaceDisplay(this.currentPool || '');
// --- NEW: Update actualRunners when runner count changes ---
this.setActualRunners();
});
this.labelRowsFlat = this.labelRows.flat();
this.selectionsSubscription = this.selectionService.selections$.subscribe(selections => {
this.currentSelections = selections;
this.maxRowsReached = selections.length >= 5;
const totalAmount = selections.reduce((sum, selection) => sum + selection.total, 0);
this.totalAmountLimitReached = totalAmount >= 5000;
this.blockedLabels = this.labelRestrictionService.getBlockedLabels(selections);
if (!this.totalAmountLimitReached) {
this.showLimitPopup = false;
}
});
this.currentRowSubscription = this.selectionService.currentRow$.subscribe(row => {
this.currentTotal = row.total;
});
// --- NEW: Subscribe to race changes ---
this.sharedStateService.selectedRace$.subscribe(race => {
this.selectedRaceNumber = String(race || '1');
this.setActualRunners();
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();
}
});
const data = localStorage.getItem('raceCardData');
if (data) {
this.raceCardData = JSON.parse(data);
} else {
this.raceCardData = {};
}
}
ngOnDestroy() {
this.currentRowSubscription?.unsubscribe();
this.selectionsSubscription?.unsubscribe();
this.runnerCountSubscription?.unsubscribe();
}
// --- NEW HELPER METHOD ---
getActualRunnersForCurrentPoolLeg(): Set<number> {
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const raceIdx = this.getRaceForLeg(this.currentPool!, this.multiLegStage) - 1;
const race = raceCardData?.raceVenueRaces?.races?.[raceIdx];
if (race?.runners && Array.isArray(race.runners)) {
return new Set(race.runners.map((r: any) => Number(r.number)));
}
if (Array.isArray(race)) {
return new Set(race.map((r: any) => Number(r.number || r)));
}
return new Set();
}
// --- MODIFIED METHOD ---
setActualRunners() {
if (this.currentPool && this.multiLegLabels.includes(this.selectedLabel || '')) {
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
} else {
this.actualRunners = this.getActualRunnersForCurrentRace();
}
}
// --- NEW METHOD ---
getActualRunnersForCurrentRace(): Set<number> {
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const selectedRaceIdx = this.sharedStateService.getSelectedRace() - 1;
const race = raceCardData?.raceVenueRaces?.races?.[selectedRaceIdx];
if (race?.runners && Array.isArray(race.runners)) {
return new Set(race.runners.map((r: any) => Number(r.number)));
}
if (Array.isArray(race)) {
return new Set(race.map((r: any) => Number(r.number || r)));
}
return new Set();
}
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 (['FOR', 'QUI', 'TAN'].includes(label) && this.isBoxed) {
return false;
}
const specialLabels = ['FOR', 'QUI', 'TAN', 'EXA', 'TRE', 'MJP', 'JKP', '.'];
return specialLabels.includes(label);
}
get isShashEnterDisabled(): boolean {
if (this.selectedLabel === 'TAN') {
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 = ['WIN', 'SHP', 'THP', 'PLC', 'WSP','SHW', 'TRE', 'MJP', 'JKP'];
if (disallowedBoxLabels.includes(this.selectedLabel)) {
return true;
}
if (this.selectedNumbers.includes('F')) {
return true;
}
return false;
}
get isWSPSelection(): boolean {
const currentRow = this.selectionService.getCurrentRow();
return ['WIN', 'SHP', 'PLC'].includes(currentRow.label) ||
this.selectionService.getSelections().some(sel => ['WIN', 'SHP', 'PLC'].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 {
return this.disabledLabels.includes(label) ||
this.totalAmountLimitReached ||
this.blockedLabels.has(label);
}
// --- MODIFIED METHOD ---
isNumberDisabled(number: number): boolean {
// Disable if number is not in actualRunners
if (!this.actualRunners.has(number)) {
return true;
}
// Allow all numbers for TAN when boxed
if (this.selectedLabel === 'TAN' && this.isBoxed) {
return false;
}
// Allow selection for TAN, multi-leg, or two-group labels
if (
this.selectedLabel === 'TAN' ||
this.multiLegLabels.includes(this.selectedLabel || '') ||
this.twoGroupLabels.includes(this.selectedLabel || '')
) {
return false;
}
// Disable if number is already selected or total amount limit reached
return this.selectedNumbers.includes(number) || this.totalAmountLimitReached;
}
selectLabel(label: string) {
if (this.totalAmountLimitReached || this.blockedLabels.has(label)) {
this.showLimitPopup = true;
return;
}
if (label === 'TRE') {
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 poolName = label === 'MJP' ? 'mjp1' : label === 'JKP' ? 'jkp1' : label === 'TRE' ? 'trb1' : label;
this.currentPool = poolName;
this.multiLegBaseRaceIdx = this.getBaseRaceIndexForPool(poolName);
// Broadcast race and pool info for navbar
this.sharedStateService.updateSharedData({
type: 'multiLegPoolStart',
value: { label: poolName, baseRaceIdx: this.multiLegBaseRaceIdx }
});
this.updateLegRaceDisplay(poolName);
// --- NEW: Update runners and number pad immediately ---
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
this.runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, 0) || 12;
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
this.numbersFlat = this.numberRows.flat();
} else {
this.currentPool = null;
this.multiLegBaseRaceIdx = 0;
this.currentLegRaceDisplay = '';
this.sharedStateService.updateSharedData({
type: 'multiLegPoolEnd',
value: null
});
// --- NEW: Update actualRunners for single race ---
this.setActualRunners();
}
//----------------------------------ADDED THIS -----------------------------------------------------
if (label === 'WSP') {
const wspLabels = ['WIN', 'SHP', 'PLC'];
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 totalNew = 0;
const totalExisting = currentSelections.reduce((sum, r) => sum + r.total, 0);
if (totalExisting + totalNew <= 5000) {
this.selectionService.setSelections([...currentSelections, ...blankRows]);
}
}
this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0 });
return;
}
//----------------------------------ended here----------------------------------------------------
// Reset TAN
this.tanGroupStage = 0;
this.tanGroups = [[], [], []];
// Reset FOR/QUI
this.isFirstGroupComplete = false;
this.firstGroup = [];
this.secondGroup = [];
// Reset Multi-leg
this.multiLegStage = 0;
this.multiLegGroups = [[], [], [], [], []];
this.selectionService.updatePartial({ label });
}
private getBaseRaceIndexForPool(poolName: string): number {
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const totalRaces = raceCardData?.raceVenueRaces?.races?.length || 10;
const maxLegs = this.getMaxLegs(poolName);
// Try to get pool-to-race mapping from raceCardData
const poolRaces = raceCardData?.raceVenueRaces?.pools?.[poolName] || [];
let baseRaceIdx = poolRaces.length > 0 ? poolRaces[0] : this.getDefaultBaseRace(poolName);
// Ensure enough races remain for the pool
if (baseRaceIdx + maxLegs - 1 > totalRaces) {
baseRaceIdx = Math.max(1, totalRaces - maxLegs + 1);
}
return baseRaceIdx;
}
private getDefaultBaseRace(poolName: string): number {
// Fallback to hardcoded values if raceCardData.pools is unavailable
const poolRaceMap: { [key: string]: number } = {
'mjp1': 1,
'jkp1': 3,
'trb1': 2,
'trb2': 5
};
return poolRaceMap[poolName] || this.sharedStateService.getSelectedRace();
}
selectNumber(number: number) {
if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return;
// TAN Box mode
if (this.selectedLabel === 'TAN' && 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: 'TAN'
});
}
return;
}
// TAN unboxed
if (this.selectedLabel === 'TAN') {
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 logic (TRE, MJP, JKP)
if (this.multiLegLabels.includes(this.selectedLabel)) {
if (!this.multiLegGroups[this.multiLegStage].includes(number)) {
this.multiLegGroups[this.multiLegStage].push(number);
this.updateMultiLegSelection();
}
return;
}
// FOR/QUI logic
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-number selection (WIN, SHP, THP, etc.)
if (!this.selectedNumbers.includes(number)) {
this.selectedNumbers.push(number);
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
// ✅ Special logic: If WSP, mirror number to WIN, SHP, and THP
if (this.selectedLabel === 'WSP') {
const labelsToUpdate = ['WIN', 'SHP', 'PLC'];
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);
}
}
}
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: 'TRE' | 'MJP' | 'JKP', 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 || ['WIN', 'SHP', 'PLC'].includes(base.label)) {
return [];
}
return ['WIN', 'SHP', 'PLC'].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 (['TAN', 'FOR', 'QUI'].includes(row.label)) {
combinations = row.numbers.length * (row.numbers.length - 1);
} else if (['TRE', 'JKP', '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 = ['WIN', 'SHP', 'PLC'];
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);
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 = ['WIN', 'SHP', 'PLC'];
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]; // Synchronize selectedNumbers
}
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);
// Only increment stage if not at the last stage (PLC)
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 === 'TAN' && this.isBoxed) {
return;
}
if (this.selectedLabel === 'TAN') {
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 = ['WIN', 'SHP', 'PLC'];
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);
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)
);
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() {
// Disable Enter if maxRowsReached
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 (WIN, SHP, PLC) to have valid numbers and values >= 1
const wspLabels = ['WIN', 'SHP', 'PLC'];
const wspSelections = selections.filter(sel => wspLabels.includes(sel.label));
// Check if we are at the last stage (PLC) and all rows are valid
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);
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 'TRE' | 'MJP' | 'JKP',
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 => !['WIN', 'SHP', 'PLC'].includes(sel.label));
this.selectionService.setSelections([...nonWSPSelections, ...virtualRows]);
}
this.selectionService.finalizeCurrentRow();
this.resetSelections();
}
//---------Helper Function-----------
getHorseNumbersForSelectedRace(): number[] {
try {
const raceCardDataStr = localStorage.getItem('raceCardData');
console.log('[DEBUG] raceCardDataStr:', raceCardDataStr);
if (!raceCardDataStr) {
console.warn('[DEBUG] No raceCardData found in localStorage');
return [];
}
const raceCardData = JSON.parse(raceCardDataStr);
console.log('[DEBUG] Parsed raceCardData:', raceCardData);
const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10) - 1; // Convert '1' → 0
console.log('[DEBUG] selectedRaceNumber:', this.selectedRaceNumber);
console.log('[DEBUG] selectedRaceIdx:', selectedRaceIdx);
const races = raceCardData.raceVenueRaces?.races || [];
console.log('[DEBUG] races array:', races);
if (races[selectedRaceIdx]) {
console.log('[DEBUG] Horse numbers for selected race:', races[selectedRaceIdx]);
return races[selectedRaceIdx];
} else {
console.warn('[DEBUG] No horses found for selectedRaceIdx:', selectedRaceIdx);
return [];
}
} catch (err) {
console.error('[DEBUG] Error parsing raceCardData:', err);
return [];
}
}
// Helper: Get horse numbers for a specific race index (0-based)
getHorseNumbersForRaceIdx(raceIdx: number): number[] {
try {
const raceCardDataStr = localStorage.getItem('raceCardData');
if (!raceCardDataStr) return [];
const raceCardData = JSON.parse(raceCardDataStr);
const races = raceCardData.raceVenueRaces?.races || [];
const race = races[raceIdx];
if (Array.isArray(race)) {
// If race is array of runners, extract their numbers
return race
.map((runner: any) => {
if (typeof runner === 'number') return runner;
if (typeof runner === 'object' && runner.number) return Number(runner.number);
if (typeof runner === 'object' && runner.horseNumber) return Number(runner.horseNumber);
return null;
})
.filter((n: any): n is number => typeof n === 'number' && n > 0);
}
return [];
} catch {
return [];
}
}
clearLocalTickets() {
localStorage.removeItem('localTickets');
console.log('🧹 localTickets cleared from localStorage');
// Clear the print count
localStorage.removeItem('printClickCount');
console.log('🧼 printClickCount cleared from localStorage');
// Reset via shared state
this.sharedStateService.setSalesTotal(0);
this.sharedStateService.setReceiveTotal(0);
// window.location.reload();
this.selectionService.clearSelections();
this.resetSelections();
// Optionally clear print clicks
// localStorage.removeItem('printClickCount');
}
//-------------------PRINT LOGIC----------------------------------------
printTicket() {
const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + sel.total, 0);
if (selectionsTotal + this.currentTotal > 5000) {
this.showLimitPopup = true;
return;
}
console.log('[DEBUG] Horse numbers for selected race:', this.getHorseNumbersForSelectedRace());
//--------------------Added Print here
console.log("🖨️ Print ticket clicked");
let selections = this.selectionService.getSelections();
const currentRow = this.selectionService.getCurrentRow();
if (this.selectedLabel === 'WSP') {
selections = selections.filter(sel => ['WIN', 'SHP', 'PLC'].includes(sel.label));
}
let allRows = [...selections];
// ✅ Just count clicks — update click count in localStorage
let clickCount = Number(localStorage.getItem('printClickCount') || '0');
clickCount += 1;
localStorage.setItem('printClickCount', clickCount.toString());
// ✅ Calculate total if currentRow is valid and not already finalized
if (currentRow.label && currentRow.numbers.length > 0 && currentRow.value > 0) {
if (!currentRow.total) {
let combinations = 1;
if (['TAN', 'FOR', 'QUI'].includes(currentRow.label)) {
combinations = currentRow.numbers.length * (currentRow.numbers.length - 1);
} else if (['TRE', 'JKP', 'MJP'].includes(currentRow.label)) {
combinations = 1; // or your specific logic
} 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.");
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 = 'MYS';
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 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 (['TRE', 'MJP', 'JKP'].includes(this.selectedLabel || '') && this.currentPool) {
ticketId = `${venue}/${fullYear}${month}${day}/${this.currentPool}`;
} else {
ticketId = `${venue}/${fullYear}${month}${day}/${this.selectedRaceNumber}`;
}
const barcodeId = `1111${day}${month}${year}${timeStr}${millis}`;
const winLabels = allRows.map(row => {
let displayNumbers = row.numbers;
// --- Multi-leg pools: Expand 'F' for each leg ---
if (['TRE', 'MJP', 'JKP'].includes(row.label)) {
// Split by '/' for legs
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);
// For each leg, expand 'F' to correct horse numbers for that leg
const poolName = row.label === 'MJP' ? 'mjp1' : row.label === 'JKP' ? 'jkp1' : row.label === 'TRE' ? 'trb1' : row.label;
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const poolRaces = raceCardData?.raceVenueRaces?.pools?.[poolName] || [];
// Fallback to hardcoded mapping if needed
const raceMap: { [key: string]: number[] } = {
'mjp1': [1, 2, 3, 4],
'jkp1': [3, 4, 5, 6, 7],
'trb1': [2, 3, 4],
'trb2': [5, 6, 7]
};
const raceIndices: number[] = poolRaces.length > 0 ? poolRaces : (raceMap[poolName] || []);
// If no mapping, fallback to consecutive races from 1
const baseRaceIdx = raceIndices.length > 0 ? raceIndices[0] : 1;
let expandedLegs: string[] = legs.map((leg, i) => {
// Find race index for this leg
let raceIdx = raceIndices.length > i ? raceIndices[i] - 1 : (baseRaceIdx - 1 + i);
let expanded = leg.flatMap(n =>
n === 'F'
? this.getHorseNumbersForRaceIdx(raceIdx).map(num => num.toString())
: [n]
);
// Remove '-' and '#' for display
return expanded.filter(n => n !== '-' && n !== '#').join(',');
});
// Join legs with '/'
let numbersStr = expandedLegs.join(' / ');
const label = row.label.padEnd(10);
const numbers = numbersStr.padEnd(15);
const value = (`*${row.value || 0}`).padEnd(8);
return `${label}${numbers} ${value}`;
}
// 🐎 Expand 'F' to full horse numbers for other pools
if (displayNumbers.includes('F')) {
displayNumbers = displayNumbers.flatMap(n =>
n === 'F' ? this.getHorseNumbersForSelectedRace().map(num => num.toString()) : [n]
);
}
let numbersStr = '';
// 🎯 FOR, QUI, TAN logic with box check
if (['FOR', 'QUI'].includes(row.label)) {
const actualNumbers = displayNumbers.filter(n => n !== '#' && n !== '-').join(',');
if (row.numbers.includes('#') || row.isBoxed) { // ✅ box condition
numbersStr = `${actualNumbers} - ${actualNumbers}`;
} else {
numbersStr = actualNumbers;
}
} else {
// 📝 All other pools
numbersStr = displayNumbers.filter(n => n !== '#' && n !== '-').join(',');
}
const label = row.label.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');
//------------------------------------EIN LABELS ENDS HERE --------------------------------
// ✅ Print preview
const printData = {
ticketId,
barcodeId,
venue,
date: `${day}-${month}-${fullYear}`,
winLabels,
ticketCount,
totalAmount,
gstNumber: '29ABCDE1234F2Z5'
};
// 🧾 Simulated console ticket
console.log('--- Simulated Ticket Print ---');
console.log(`Ticket ID : ${printData.ticketId}`);
console.log(`Barcode ID : ${printData.barcodeId}`);
console.log(`|||||| ||| | ||||| |||| |`); // Dummy barcode
console.log(`WIN Labels :`);
printData.winLabels.split('\n').forEach(row => console.log(row));
console.log(`*${printData.ticketCount}${printData.totalAmount}`);
console.log(`GST Number : ${printData.gstNumber}`);
console.log(`Date/Time : ${now.toLocaleString()}`);
console.log('-----------------------------');
// 🖨️ Send to printer API
const payload = {
type: 'ticket',
ticketId: printData.ticketId,
barcodeId: printData.barcodeId,
winLabels: printData.winLabels,
ticketCount: printData.ticketCount,
totalAmount: printData.totalAmount,
gstNumber: printData.gstNumber,
dateTime: now.toLocaleString()
};
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);
}
//-------------------LOG PRINT DETAILS --------------------------------------------------
try {
const existingTicketsStr = localStorage.getItem('localTicketsViewlog');
const existingTickets = existingTicketsStr ? JSON.parse(existingTicketsStr) : [];
// ⛔️ If there are already 10 tickets, remove the oldest (index 0)
if (existingTickets.length >= 10) {
existingTickets.shift(); // remove 0th element
}
// ✅ Push the new ticket
existingTickets.push(payload);
// 📝 Save updated list back to localStorage
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).');
} 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(); // ✅ Clear selections after successful print
})
.catch(error => {
console.error("❌ Print failed:", error);
this.erase(); // ✅ Clear selections after successful print
});
this.erase(); // ✅ Clear selections after successful print
//--------------------Ended Print here -----------------------------
this.selectionService.finalizeCurrentRow();
this.resetSelections();
}
//----------------------------------PRINT ENDS HERE ------------------------------------------------------
erase() {
this.selectionService.clearSelections();
this.resetSelections();
}
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;
// Clear multi-leg display in Navbar
this.sharedStateService.updateSharedData({
type: 'multiLegPoolEnd',
value: null
});
}
toggleBoxMode() {
if (this.totalAmountLimitReached) return;
this.isBoxed = !this.isBoxed;
const value = parseFloat(this.padValue) || 0;
if (this.selectedLabel === 'TAN' && 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 === 'TAN' && 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: 'TAN'
});
}
return;
}
if (this.selectedLabel === 'TAN') {
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(poolName: string): number {
switch (poolName) {
case 'mjp1':
return 4;
case 'jkp1':
return 5;
case 'trb1':
case 'trb2':
case 'TRE':
return 3;
default:
return 5; // Default to 5 for unspecified pools
}
}
private getRaceForLeg(poolName: string, leg: number): number {
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const poolRaces = raceCardData?.raceVenueRaces?.pools?.[poolName] || [];
if (poolRaces.length > leg) {
return poolRaces[leg];
}
// Fallback to default race mapping
const raceMap: { [key: string]: number[] } = {
'mjp1': [1, 2, 3, 4],
'jkp1': [3, 4, 5, 6, 7],
'trb1': [2, 3, 4],
'trb2': [5, 6, 7]
};
return raceMap[poolName]?.[leg] || (this.multiLegBaseRaceIdx + leg);
}
private updateLegRaceDisplay(poolName: string) {
if (!['mjp1', 'jkp1', 'trb1', 'trb2'].includes(poolName)) {
this.currentLegRaceDisplay = '';
this.currentPool = null;
return;
}
const raceIdx = this.getRaceForLeg(poolName, this.multiLegStage);
this.currentLegRaceDisplay = `Leg ${this.multiLegStage + 1} (Race ${raceIdx})`;
const runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, this.multiLegStage);
this.sharedStateService.setRunnerCount(runnerCount);
this.sharedStateService.updateSharedData({
type: 'currentLegRace',
value: raceIdx
});
}
private getRunnerCountForLeg(baseIdx: number, leg: number): number {
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const raceIdx = this.getRaceForLeg(this.currentPool || '', leg) - 1;
const race = raceCardData?.raceVenueRaces?.races?.[raceIdx] || [];
if (race?.runners && Array.isArray(race.runners)) {
return race.runners.length;
}
if (Array.isArray(race)) {
return race.length;
}
return 12;
}
private handleFieldForSpecialLabels() {
if (!this.selectedLabel) return;
if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') {
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 === 'TAN') {
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 === 'FOR' || this.selectedLabel === 'QUI') {
if (!this.isFirstGroupComplete && this.firstGroup.length === 0) {
return true;
}
if (this.isFirstGroupComplete && this.secondGroup.length === 0) {
return true;
}
return false;
}
if (this.selectedLabel === 'TAN' || this.multiLegLabels.includes(this.selectedLabel || '')) {
if (this.selectedLabel === 'TAN' && 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 (['FOR', 'QUI', 'TAN'].includes(this.selectedLabel || '') || this.multiLegLabels.includes(this.selectedLabel || '')) {
this.handleFieldForSpecialLabels();
} else {
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';
}
} else {
this.fieldInput += key;
}
}
confirmFieldEntry() {
if (!this.fieldFEntered || !this.selectedLabel) return;
if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') {
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
});
} else if (this.selectedLabel === 'TAN') {
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
});
} else {
this.selectedNumbers = ['F'];
this.selectionService.updatePartial({
label: this.selectedLabel,
numbers: ['F'],
isBoxed: false
});
}
this.closeFieldModal();
}
openPoolReplaceModal() {
if (this.totalAmountLimitReached) return;
// Determine allowed group based on current selection
const groupA = ['WIN', 'SHP', 'THP', 'PLC', 'SHW'];
const groupB = ['FOR', 'QUI', 'TAN'];
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;
// Update selection service with new label
this.selectionService.updatePartial({ label });
}
closePoolReplaceModal() {
this.poolReplaceOpen = false;
}
treButtonClick(btnNum: number) {
this.trePopupVisible = false;
this._selectTreAfterPopup(btnNum);
}
private _selectTreAfterPopup(btnNum: number) {
this.selectedLabel = 'TRE';
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 TRE button to specific pool name and base race dynamically
const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}');
const trePoolMap: { [key: number]: { name: string } } = {
1: { name: 'trb1' },
2: { name: 'trb2' }
};
const poolInfo = trePoolMap[btnNum] || { name: 'trb1' };
this.currentPool = poolInfo.name;
this.multiLegBaseRaceIdx = this.getBaseRaceIndexForPool(poolInfo.name);
// Broadcast TRE selection
this.sharedStateService.updateSharedData({
type: 'multiLegPoolStart',
value: { label: poolInfo.name, baseRaceIdx: this.multiLegBaseRaceIdx }
});
this.updateLegRaceDisplay(poolInfo.name);
this.selectionService.updatePartial({ label: 'TRE' });
// --- NEW: Update runners and number pad immediately ---
this.actualRunners = this.getActualRunnersForCurrentPoolLeg();
this.runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, 0) || 12;
this.numbers = Array.from({ length: 30 }, (_, i) => i + 1);
this.numbersFlat = this.numberRows.flat();
}
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);
});
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) {
console.log('[DEBUG] canEnterRow: maxRowsReached is true, disabling Enter');
return false;
}
// Special handling for WSP
if (this.selectedLabel === 'WSP') {
const isValidPadValue = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue);
const hasNumbers = this.selectedNumbers.length > 0;
console.log('[DEBUG] canEnterRow (WSP):', {
isValidPadValue,
hasNumbers,
padValue: this.padValue,
selectedNumbers: this.selectedNumbers,
wspTicketStage: this.wspTicketStage
});
return isValidPadValue && hasNumbers;
}
// Default logic for non-WSP
const currentRow = this.selectionService.getCurrentRow();
const result = !!currentRow.label &&
!!currentRow.numbers &&
currentRow.numbers.length > 0 &&
typeof currentRow.value === 'number' &&
currentRow.value >= 1 &&
currentRow.value <= 100 &&
currentRow.total > 0;
console.log('[DEBUG] canEnterRow (non-WSP):', {
label: currentRow.label,
numbers: currentRow.numbers,
value: currentRow.value,
total: currentRow.total,
result
});
return result;
}
}