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

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 */ }
}
}