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(); actualRunners: Set = 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 = 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 { 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 { 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(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 = { 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 */ } } }