import { Component, Input, OnInit, OnDestroy, NgZone, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Subscription } from 'rxjs'; import { SelectionService, SelectionData } from '../selection.service/selection.service'; import { SharedStateService } from '../../service/shared-state.service'; import { LabelRestrictionService } from '../selection.service/label-restriction.service'; import _ from 'lodash'; @Component({ selector: 'app-touch-pad-menu', standalone: true, imports: [CommonModule], templateUrl: './touch-pad-menu.component.html', styleUrls: ['./touch-pad-menu.component.css'], changeDetection: ChangeDetectionStrategy.OnPush // ๐Ÿ”ฅ CRUCIAL for performance }) export class TouchPadMenuComponent implements OnInit, OnDestroy { @Input() ticketingActive: boolean = false; public twoGroupLabels = ['FOR', 'QUI']; public multiLegLabels = ['TRE', 'MJP', 'JKP']; public threeGroupLabels = ['TAN']; public allowedFieldLabels = ['WIN', 'SHP', 'THP', 'PLC', 'SHW']; labels: string[] = [ 'WIN', 'SHP', 'THP', 'PLC', 'SHW', 'FOR', 'QUI', 'TAN', 'EXA', 'WSP', 'TRE', 'MJP', 'JKP', 'SJP', '.' ]; numbers: number[] = Array.from({ length: 30 }, (_, i) => i + 1); runnerCount: number = 12; labelRowsFlat: string[] = []; numbersFlat: number[] = []; blockedLabels = new Set(); 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', '.']; // TAN logic tanGroupStage = 0; tanGroups: (number | string)[][] = [[], [], []]; // FOR/QUI logic isFirstGroupComplete = false; firstGroup: (number | string)[] = []; secondGroup: (number | string)[] = []; // Multi-leg logic (TRE, MJP, JKP) multiLegStage = 0; multiLegGroups: (number | string)[][] = [[], [], [], [], []]; multiLegBaseRaceIdx: number = 0; // Track starting race index currentLegRaceDisplay: string = ''; // Display current leg's race currentPool: string | null = null; // Track current pool (mjp1, jkp1, trb1, trb2) isBoxed: boolean = false; // FIELD modal fieldModalOpen = false; fieldInput: string = ''; fieldFEntered = false; // POOL REPLACE modal poolReplaceOpen = false; poolReplaceOptions: string[] = []; // TRE popup trePopupVisible = false; private currentRowSubscription: Subscription | null = null; private selectionsSubscription: Subscription | null = null; private runnerCountSubscription: Subscription | null = null; private currentTotal: number = 0; private currentSelections: SelectionData[] = []; enabledHorseNumbers: number[] = []; prevEnabledKey: string = ''; raceCardData: any = {}; constructor( private selectionService: SelectionService, private sharedStateService: SharedStateService, private labelRestrictionService: LabelRestrictionService, private ngZone: NgZone // <-- inject NgZone ) {} selectedRaceNumber: string = '1'; // Default ngOnInit() { this.runnerCountSubscription = this.sharedStateService.runnerCount$.subscribe(count => { this.runnerCount = count || 12; this.numbers = Array.from({ length: 30 }, (_, i) => i + 1); this.numbersFlat = this.numberRows.flat(); this.updateLegRaceDisplay(this.currentPool || ''); // --- NEW: Update actualRunners when runner count changes --- this.setActualRunners(); }); this.labelRowsFlat = this.labelRows.flat(); this.selectionsSubscription = this.selectionService.selections$.subscribe(selections => { this.currentSelections = selections; this.maxRowsReached = selections.length >= 5; const totalAmount = selections.reduce((sum, selection) => sum + selection.total, 0); this.totalAmountLimitReached = totalAmount >= 5000; this.blockedLabels = this.labelRestrictionService.getBlockedLabels(selections); if (!this.totalAmountLimitReached) { this.showLimitPopup = false; } }); this.currentRowSubscription = this.selectionService.currentRow$.subscribe(row => { this.currentTotal = row.total; }); // --- NEW: Subscribe to race changes --- this.sharedStateService.selectedRace$.subscribe(race => { this.selectedRaceNumber = String(race || '1'); this.setActualRunners(); if (this.currentPool && this.multiLegLabels.includes(this.selectedLabel || '')) { this.updateLegRaceDisplay(this.currentPool); const runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, this.multiLegStage); this.runnerCount = runnerCount || 12; this.numbers = Array.from({ length: 30 }, (_, i) => i + 1); this.numbersFlat = this.numberRows.flat(); this.actualRunners = this.getActualRunnersForCurrentPoolLeg(); } else { this.setActualRunners(); } }); const data = localStorage.getItem('raceCardData'); if (data) { this.raceCardData = JSON.parse(data); } else { this.raceCardData = {}; } } ngOnDestroy() { this.currentRowSubscription?.unsubscribe(); this.selectionsSubscription?.unsubscribe(); this.runnerCountSubscription?.unsubscribe(); } // --- NEW HELPER METHOD --- getActualRunnersForCurrentPoolLeg(): Set { const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const raceIdx = this.getRaceForLeg(this.currentPool!, this.multiLegStage) - 1; const race = raceCardData?.raceVenueRaces?.races?.[raceIdx]; if (race?.runners && Array.isArray(race.runners)) { return new Set(race.runners.map((r: any) => Number(r.number))); } if (Array.isArray(race)) { return new Set(race.map((r: any) => Number(r.number || r))); } return new Set(); } // --- MODIFIED METHOD --- setActualRunners() { if (this.currentPool && this.multiLegLabels.includes(this.selectedLabel || '')) { this.actualRunners = this.getActualRunnersForCurrentPoolLeg(); } else { this.actualRunners = this.getActualRunnersForCurrentRace(); } } // --- NEW METHOD --- getActualRunnersForCurrentRace(): Set { const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const selectedRaceIdx = this.sharedStateService.getSelectedRace() - 1; const race = raceCardData?.raceVenueRaces?.races?.[selectedRaceIdx]; if (race?.runners && Array.isArray(race.runners)) { return new Set(race.runners.map((r: any) => Number(r.number))); } if (Array.isArray(race)) { return new Set(race.map((r: any) => Number(r.number || r))); } return new Set(); } get labelRows() { return this.chunk(this.labels, 3); } get numberRows() { return this.chunk(this.numbers, 6); } get numericPadEnabled() { return this.selectedLabel !== null && (this.selectedNumbers.length > 0 || this.selectedNumbers.includes('F')); } get showShashEnter(): boolean { const label = this.selectedLabel || ''; if (['FOR', 'QUI', 'TAN'].includes(label) && this.isBoxed) { return false; } const specialLabels = ['FOR', 'QUI', 'TAN', 'EXA', 'TRE', 'MJP', 'JKP', '.']; if (this.multiLegLabels.includes(label)) { const maxLegs = this.getMaxLegs(this.currentPool || ''); // Hide Shash Enter if on the final leg return this.multiLegStage < maxLegs - 1; } return specialLabels.includes(label); } get isShashEnterDisabled(): boolean { if (this.selectedLabel === 'TAN') { if (this.isBoxed) { return true; } return this.tanGroupStage >= 2 || this.tanGroups[this.tanGroupStage].length === 0; } else if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); return this.multiLegStage >= maxLegs || this.multiLegGroups[this.multiLegStage].length === 0; } else if (this.twoGroupLabels.includes(this.selectedLabel || '')) { return this.isFirstGroupComplete || this.firstGroup.length === 0; } return false; } get showBackspace(): boolean { return this.selectedLabel !== null && (this.selectedNumbers.length > 0 || this.selectedNumbers.includes('F')) && this.padValue.length === 0; } get isBoxToggleDisabled(): boolean { if (!this.selectedLabel) return true; const disallowedBoxLabels = ['WIN', 'SHP', 'THP', 'PLC', 'WSP', 'SHW', 'TRE', 'MJP', 'JKP']; if (disallowedBoxLabels.includes(this.selectedLabel)) { return true; } if (this.selectedNumbers.includes('F')) { return true; } // Disable Box toggle for TAN, QUI, FOR after any number is selected if (['TAN', 'QUI', 'FOR'].includes(this.selectedLabel) && this.selectedNumbers.length > 0) { return true; } return false; } get isWSPSelection(): boolean { const currentRow = this.selectionService.getCurrentRow(); return ['WIN', 'SHP', 'PLC'].includes(currentRow.label) || this.selectionService.getSelections().some(sel => ['WIN', 'SHP', 'PLC'].includes(sel.label)); } private chunk(array: T[], size: number): T[][] { return Array.from({ length: Math.ceil(array.length / size) }, (_, i) => array.slice(i * size, i * size + size) ); } isLabelDisabled(label: string): boolean { return this.disabledLabels.includes(label) || this.totalAmountLimitReached || this.blockedLabels.has(label); } // --- MODIFIED METHOD --- isNumberDisabled(number: number): boolean { // Disable if number is not in actualRunners if (!this.actualRunners.has(number)) { return true; } // Disable if total amount limit reached if (this.totalAmountLimitReached) { return true; } // Allow all numbers for TAN when boxed, but disable selected numbers if (this.selectedLabel === 'TAN' && this.isBoxed) { return this.selectedNumbers.includes(number); } // TAN (unboxed): Disable numbers already selected in the current group if (this.selectedLabel === 'TAN') { 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 (FOR, QUI): Disable numbers already selected in the current group if (this.twoGroupLabels.includes(this.selectedLabel || '')) { if (!this.isFirstGroupComplete) { return this.firstGroup.includes(number); } else { return this.secondGroup.includes(number); } } // Default case for WIN, SHP, THP, PLC, etc.: Disable if already selected return this.selectedNumbers.includes(number); } selectLabel(label: string) { if (this.totalAmountLimitReached || this.blockedLabels.has(label)) { this.showLimitPopup = true; return; } if (label === 'TRE') { this.trePopupVisible = true; return; } this.selectedLabel = label; this.selectedNumbers = []; this.padValue = ''; this.canPrint = false; this.isBoxed = false; // Store base race index and pool name for multi-leg pools if (this.multiLegLabels.includes(label)) { const poolName = label === 'MJP' ? 'mjp1' : label === 'JKP' ? 'jkp1' : label === 'TRE' ? 'trb1' : label; this.currentPool = poolName; this.multiLegBaseRaceIdx = this.getBaseRaceIndexForPool(poolName); // Broadcast race and pool info for navbar this.sharedStateService.updateSharedData({ type: 'multiLegPoolStart', value: { label: poolName, baseRaceIdx: this.multiLegBaseRaceIdx } }); this.updateLegRaceDisplay(poolName); // --- NEW: Update runners and number pad immediately --- this.actualRunners = this.getActualRunnersForCurrentPoolLeg(); this.runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, 0) || 12; this.numbers = Array.from({ length: 30 }, (_, i) => i + 1); this.numbersFlat = this.numberRows.flat(); } else { this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null }); // --- NEW: Update actualRunners for single race --- this.setActualRunners(); } //----------------------------------ADDED THIS ----------------------------------------------------- if (label === 'WSP') { const wspLabels = ['WIN', 'SHP', 'PLC']; this.wspTicketStage = 0; this.selectionService.finalizeCurrentRow(); const currentSelections = this.selectionService.getSelections(); const existingWSP = currentSelections.filter(sel => wspLabels.includes(sel.label)); if (existingWSP.length === 0) { const blankRows = wspLabels.map(lbl => ({ label: lbl, numbers: [], value: 0, total: 0, isBoxed: false })); const totalNew = 0; const totalExisting = currentSelections.reduce((sum, r) => sum + r.total, 0); if (totalExisting + totalNew <= 5000) { this.selectionService.setSelections([...currentSelections, ...blankRows]); } } this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0 }); return; } //----------------------------------ended here---------------------------------------------------- // Reset TAN this.tanGroupStage = 0; this.tanGroups = [[], [], []]; // Reset FOR/QUI this.isFirstGroupComplete = false; this.firstGroup = []; this.secondGroup = []; // Reset Multi-leg this.multiLegStage = 0; this.multiLegGroups = [[], [], [], [], []]; this.selectionService.updatePartial({ label }); } private getBaseRaceIndexForPool(poolName: string): number { const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const totalRaces = raceCardData?.raceVenueRaces?.races?.length || 10; const maxLegs = this.getMaxLegs(poolName); // Try to get pool-to-race mapping from raceCardData const poolRaces = raceCardData?.raceVenueRaces?.pools?.[poolName] || []; let baseRaceIdx = poolRaces.length > 0 ? poolRaces[0] : this.getDefaultBaseRace(poolName); // Ensure enough races remain for the pool if (baseRaceIdx + maxLegs - 1 > totalRaces) { baseRaceIdx = Math.max(1, totalRaces - maxLegs + 1); } return baseRaceIdx; } private getDefaultBaseRace(poolName: string): number { // Fallback to hardcoded values if raceCardData.pools is unavailable const poolRaceMap: { [key: string]: number } = { 'mjp1': 1, 'jkp1': 3, 'trb1': 2, 'trb2': 5 }; return poolRaceMap[poolName] || this.sharedStateService.getSelectedRace(); } selectNumber(number: number) { if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return; // TAN Box mode if (this.selectedLabel === 'TAN' && this.isBoxed) { if (!this.selectedNumbers.includes(number)) { const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[]; const allBoxed = [...currentNumbers, number]; const groupSize = Math.ceil(allBoxed.length / 3); const group1 = allBoxed.slice(0, groupSize); const group2 = allBoxed.slice(group1.length, group1.length + groupSize); const group3 = allBoxed.slice(group1.length + group2.length); const combined: (number | string)[] = [...group1]; if (group2.length) combined.push('-', ...group2); if (group3.length) combined.push('-', ...group3); this.selectedNumbers = combined; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers], isBoxed: true, label: 'TAN' }); } return; } // TAN unboxed if (this.selectedLabel === 'TAN') { if (!this.tanGroups[this.tanGroupStage].includes(number)) { this.tanGroups[this.tanGroupStage].push(number); const combined: (number | string)[] = [...this.tanGroups[0]]; if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]); if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]); this.selectedNumbers = combined; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); } return; } // Multi-leg logic (TRE, MJP, JKP) if (this.multiLegLabels.includes(this.selectedLabel)) { if (!this.multiLegGroups[this.multiLegStage].includes(number)) { this.multiLegGroups[this.multiLegStage].push(number); this.updateMultiLegSelection(); } return; } // FOR/QUI logic if (this.twoGroupLabels.includes(this.selectedLabel || '')) { console.log('Selected label:', this.selectedLabel); console.log('Current number clicked:', number); if (!this.isFirstGroupComplete) { console.log('First group not complete. Current firstGroup:', this.firstGroup); if (!this.firstGroup.includes(number)) { console.log(`Adding ${number} to firstGroup`); this.firstGroup.push(number); this.selectedNumbers = [...this.firstGroup]; } else { console.log(`${number} already exists in firstGroup`); } } else { console.log('First group complete. Moving to secondGroup. Current secondGroup:', this.secondGroup); if (!this.secondGroup.includes(number)) { console.log(`Adding ${number} to secondGroup`); this.secondGroup.push(number); this.selectedNumbers = [...this.firstGroup, '-', ...this.secondGroup]; } else { console.log(`${number} already exists in secondGroup`); } } console.log('Updated selectedNumbers:', this.selectedNumbers); this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); return; } // Default single-number selection (WIN, SHP, THP, etc.) if (!this.selectedNumbers.includes(number)) { this.selectedNumbers.push(number); this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); // โœ… Special logic: If WSP, mirror number to WIN, SHP, and THP if (this.selectedLabel === 'WSP') { const labelsToUpdate = ['WIN', 'SHP', 'PLC']; const selections = this.selectionService.getSelections(); const updated = selections.map(sel => { if (labelsToUpdate.includes(sel.label)) { const newNumbers = [...sel.numbers]; if (!newNumbers.includes(number)) { newNumbers.push(number); } return { ...sel, numbers: newNumbers }; } return sel; }); this.selectionService.setSelections(updated); } } } private updateMultiLegSelection() { const combined: (number | string)[] = []; for (let i = 0; i <= this.multiLegStage; i++) { if (i > 0) combined.push('/'); combined.push(...this.multiLegGroups[i]); } this.selectedNumbers = combined; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); this.updateLegRaceDisplay(this.currentPool || ''); } private calculateMultiLegAmount(poolType: 'TRE' | 'MJP' | 'JKP', horsesPerLeg: number[], units: number, unitBet: number = 10): number { return horsesPerLeg.reduce((acc, v) => acc * v, 1) * units * unitBet; } createVirtualRowsFromWSP(): SelectionData[] { const base = this.selectionService.getCurrentRow(); if (!base.numbers.length || base.value <= 0 || ['WIN', 'SHP', 'PLC'].includes(base.label)) { return []; } return ['WIN', 'SHP', 'PLC'].map(label => { const newRow: SelectionData = { ...base, label, total: 0 }; newRow.total = this.calculateTotal(newRow); return newRow; }); } private calculateTotal(row: SelectionData): number { if (!row.numbers || row.numbers.length === 0 || row.value <= 0) { return 0; } let combinations = 1; if (['TAN', 'FOR', 'QUI'].includes(row.label)) { combinations = row.numbers.length * (row.numbers.length - 1); } else if (['TRE', 'JKP', 'MJP'].includes(row.label)) { const legs = row.numbers.join('').split('/').map(leg => leg.split(',').length); combinations = legs.reduce((a, b) => a * b, 1); } else if (row.isBoxed) { const n = row.numbers.length; combinations = n > 1 ? (n * (n - 1)) / 2 : 0; } return combinations * row.value * 10; } clearWSPSelection() { if (this.selectedLabel !== 'WSP') return; const labels = ['WIN', 'SHP', 'PLC']; const targetLabel = labels[this.wspTicketStage]; // Update only the current WSP stage's value to 0 const updatedSelections = this.selectionService.getSelections().map(sel => { if (sel.label === targetLabel && JSON.stringify(sel.numbers) === JSON.stringify(this.selectedNumbers)) { return { ...sel, value: 0, total: 0 }; } return sel; }); this.selectionService.setSelections(updatedSelections); this.padValue = ''; this.updateCanPrint(); } onPadEnter() { if (this.maxRowsReached) return; if (!this.canPrint) { this.print(); } const value = parseFloat(this.padValue) || 0; if (this.selectedLabel === 'WSP') { const labels = ['WIN', 'SHP', 'PLC']; const targetLabel = labels[this.wspTicketStage]; const selections = this.selectionService.getSelections(); // Find the current WSP row to ensure numbers are synchronized const currentWSPRow = selections.find(sel => sel.label === targetLabel); if (currentWSPRow) { this.selectedNumbers = [...currentWSPRow.numbers]; // Synchronize selectedNumbers } const updatedSelections = selections.map(sel => { if (sel.label === targetLabel) { const total = value * (sel.numbers?.length || 0) * 10; return { ...sel, value, total }; } return sel; }); this.selectionService.setSelections(updatedSelections); // Only increment stage if not at the last stage (PLC) if (this.wspTicketStage < 2) { this.wspTicketStage++; // Update selectedNumbers for the next stage const nextLabel = labels[this.wspTicketStage]; const nextWSPRow = updatedSelections.find(sel => sel.label === nextLabel); if (nextWSPRow) { this.selectedNumbers = [...nextWSPRow.numbers]; } } this.padValue = ''; this.updateCanPrint(); return; } this.print(); } onShashEnter() { if (this.selectedLabel === 'TAN' && this.isBoxed) { return; } if (this.selectedLabel === 'TAN') { if (this.tanGroupStage < 2 && this.tanGroups[this.tanGroupStage].length > 0) { this.tanGroupStage++; const combined: (number | string)[] = [...this.tanGroups[0]]; if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]); if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]); this.selectedNumbers = combined; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); } return; } if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); if (this.multiLegStage < maxLegs - 1 && this.multiLegGroups[this.multiLegStage].length > 0) { this.multiLegStage++; this.updateMultiLegSelection(); this.updateLegRaceDisplay(this.currentPool || ''); // --- NEW: Update actualRunners for the new leg --- this.setActualRunners(); } return; } if (this.twoGroupLabels.includes(this.selectedLabel || '')) { if (!this.isFirstGroupComplete && this.firstGroup.length > 0) { this.isFirstGroupComplete = true; this.secondGroup = []; this.selectedNumbers = [...this.firstGroup, '-']; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); } } } //-----------------------------ENTER PAD VALUE------------------------------------- enterPadVal(key: string) { if (!this.numericPadEnabled || this.totalAmountLimitReached) return; if (key === 'X') { if (this.selectedLabel === 'WSP') { this.clearWSPSelection(); return; } this.padValue = ''; this.selectionService.updatePartial({ label: this.selectedLabel || '', numbers: [...this.selectedNumbers], value: 0, total: 0, isBoxed: this.isBoxed }); this.updateCanPrint(); return; } if (/[0-9]/.test(key)) { const currentValue = parseInt(this.padValue + key) || 0; if (currentValue > 100) return; this.padValue += key; } this.updateCanPrint(); const value = parseFloat(this.padValue) || 0; if (this.selectedLabel === 'WSP') { const labels = ['WIN', 'SHP', 'PLC']; const targetLabel = labels[this.wspTicketStage]; const updatedSelections = this.selectionService.getSelections().map(sel => { if (sel.label === targetLabel && JSON.stringify(sel.numbers) === JSON.stringify(this.selectedNumbers)) { const total = value * (sel.numbers?.length || 0) * 10; return { ...sel, value, total }; } return sel; }); this.selectionService.setSelections(updatedSelections); const currentTotal = updatedSelections.find(sel => sel.label === targetLabel)?.total || 0; if (currentTotal === 0 && value > 0) { console.log('[DEBUG] WSP row invalid (total = 0), clearing row'); this.selectionService.setSelections( updatedSelections.filter(sel => sel.label !== targetLabel || sel.numbers.length > 0) ); this.wspTicketStage = 0; this.padValue = ''; this.selectedNumbers = []; this.selectedLabel = null; this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0, isBoxed: false }); } return; } if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); const legsFilled = this.multiLegGroups.slice(0, maxLegs).every(group => group.length > 0); if (!legsFilled) { console.log('[DEBUG] Multi-leg pool incomplete (not all legs filled), clearing row'); this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0, isBoxed: false }); this.selectedLabel = null; this.selectedNumbers = []; this.padValue = ''; this.isBoxed = false; this.multiLegStage = 0; this.multiLegGroups = [[], [], [], [], []]; this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null }); this.updateCanPrint(); return; } this.selectionService.updatePartial({ value, isBoxed: this.isBoxed, label: this.selectedLabel || '', numbers: [...this.selectedNumbers] }); const currentRow = this.selectionService.getCurrentRow(); if (currentRow.total === 0 && value > 0 && currentRow.numbers.length > 0) { console.log('[DEBUG] Multi-leg row invalid (total = 0), auto-clearing current row'); this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0, isBoxed: false }); this.selectedLabel = null; this.selectedNumbers = []; this.padValue = ''; this.isBoxed = false; this.multiLegStage = 0; this.multiLegGroups = [[], [], [], [], []]; this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null }); this.updateCanPrint(); } return; } this.selectionService.updatePartial({ value, isBoxed: this.isBoxed, label: this.selectedLabel || '', numbers: [...this.selectedNumbers] }); const currentRow = this.selectionService.getCurrentRow(); if (currentRow.total === 0 && value > 0 && currentRow.numbers.length > 0) { console.log('[DEBUG] Row invalid (total = 0), auto-clearing current row'); this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0, isBoxed: false }); this.selectedLabel = null; this.selectedNumbers = []; this.padValue = ''; this.isBoxed = false; this.updateCanPrint(); } } //--------------------------------------------------------------------------------------------- updateCanPrint() { // Disable Enter if maxRowsReached if (this.maxRowsReached) { this.canPrint = false; return; } this.canPrint = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue); if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); this.canPrint = this.canPrint && this.multiLegStage === maxLegs - 1 && this.multiLegGroups[this.multiLegStage].length > 0; } } // Add this getter for print button enable logic get canPrintTicket(): boolean { // At least one valid row in finalized selections or current row const selections = this.selectionService.getSelections(); const currentRow = this.selectionService.getCurrentRow(); if (this.selectedLabel === 'WSP') { // For WSP, require all three rows (WIN, SHP, PLC) to have valid numbers and values >= 1 const wspLabels = ['WIN', 'SHP', 'PLC']; const wspSelections = selections.filter(sel => wspLabels.includes(sel.label)); // Check if we are at the last stage (PLC) and all rows are valid const allWSPRowsValid = wspSelections.length === 3 && wspSelections.every(row => row.label && row.numbers && row.numbers.length > 0 && row.value >= 1 && row.total > 0 ); return this.wspTicketStage === 2 && allWSPRowsValid; } // For non-WSP, keep existing logic: any valid row enables printing const hasValidRow = selections.some( row => !!row.label && !!row.numbers && row.numbers.length > 0 && row.value > 0 && row.total > 0 ) || ( !!currentRow.label && !!currentRow.numbers && currentRow.numbers.length > 0 && currentRow.value > 0 && currentRow.total > 0 ); return Boolean(hasValidRow); } print() { const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + sel.total, 0); let currentRowAmount = 0; if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); const horsesPerLeg = this.multiLegGroups.map((group, index) => { if (group.includes('F')) { return this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, index); } return group.length; }).slice(0, maxLegs); const units = parseFloat(this.padValue) || 0; currentRowAmount = this.calculateMultiLegAmount( this.selectedLabel as 'TRE' | 'MJP' | 'JKP', horsesPerLeg, units ); if (currentRowAmount > 5000 || selectionsTotal + currentRowAmount > 5000) { this.totalAmountLimitReached = true; this.showLimitPopup = true; return; } // Ensure all legs have selections if (horsesPerLeg.some(count => count === 0)) { return; } } else { currentRowAmount = this.currentTotal; if (selectionsTotal + currentRowAmount > 5000) { this.totalAmountLimitReached = true; this.showLimitPopup = true; return; } } if (this.selectedLabel === 'WSP') { const virtualRows = this.createVirtualRowsFromWSP(); const currentSelections = this.selectionService.getSelections(); const nonWSPSelections = currentSelections.filter(sel => !['WIN', 'SHP', 'PLC'].includes(sel.label)); this.selectionService.setSelections([...nonWSPSelections, ...virtualRows]); } this.selectionService.finalizeCurrentRow(); this.resetSelections(); } //---------Helper Function----------- getHorseNumbersForSelectedRace(): number[] { try { const raceCardDataStr = localStorage.getItem('raceCardData'); console.log('[DEBUG] raceCardDataStr:', raceCardDataStr); if (!raceCardDataStr) { console.warn('[DEBUG] No raceCardData found in localStorage'); return []; } const raceCardData = JSON.parse(raceCardDataStr); console.log('[DEBUG] Parsed raceCardData:', raceCardData); const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10) - 1; // Convert '1' โ†’ 0 console.log('[DEBUG] selectedRaceNumber:', this.selectedRaceNumber); console.log('[DEBUG] selectedRaceIdx:', selectedRaceIdx); const races = raceCardData.raceVenueRaces?.races || []; console.log('[DEBUG] races array:', races); if (races[selectedRaceIdx]) { console.log('[DEBUG] Horse numbers for selected race:', races[selectedRaceIdx]); return races[selectedRaceIdx]; } else { console.warn('[DEBUG] No horses found for selectedRaceIdx:', selectedRaceIdx); return []; } } catch (err) { console.error('[DEBUG] Error parsing raceCardData:', err); return []; } } // Helper: Get horse numbers for a specific race index (0-based) getHorseNumbersForRaceIdx(raceIdx: number): number[] { try { const raceCardDataStr = localStorage.getItem('raceCardData'); if (!raceCardDataStr) return []; const raceCardData = JSON.parse(raceCardDataStr); const races = raceCardData.raceVenueRaces?.races || []; const race = races[raceIdx]; if (Array.isArray(race)) { // If race is array of runners, extract their numbers return race .map((runner: any) => { if (typeof runner === 'number') return runner; if (typeof runner === 'object' && runner.number) return Number(runner.number); if (typeof runner === 'object' && runner.horseNumber) return Number(runner.horseNumber); return null; }) .filter((n: any): n is number => typeof n === 'number' && n > 0); } return []; } catch { return []; } } clearLocalTickets() { localStorage.removeItem('localTickets'); console.log('๐Ÿงน localTickets cleared from localStorage'); // Clear the print count localStorage.removeItem('printClickCount'); console.log('๐Ÿงผ printClickCount cleared from localStorage'); // Reset via shared state this.sharedStateService.setSalesTotal(0); this.sharedStateService.setReceiveTotal(0); // window.location.reload(); this.selectionService.clearSelections(); this.resetSelections(); // Optionally clear print clicks // localStorage.removeItem('printClickCount'); } //-------------------PRINT LOGIC---------------------------------------- printTicket() { const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + sel.total, 0); if (selectionsTotal + this.currentTotal > 5000) { this.showLimitPopup = true; return; } console.log('[DEBUG] Horse numbers for selected race:', this.getHorseNumbersForSelectedRace()); //--------------------Added Print here console.log("๐Ÿ–จ๏ธ Print ticket clicked"); let selections = this.selectionService.getSelections(); const currentRow = this.selectionService.getCurrentRow(); if (this.selectedLabel === 'WSP') { selections = selections.filter(sel => ['WIN', 'SHP', 'PLC'].includes(sel.label)); } let allRows = [...selections]; // โœ… Just count clicks โ€” update click count in localStorage let clickCount = Number(localStorage.getItem('printClickCount') || '0'); clickCount += 1; localStorage.setItem('printClickCount', clickCount.toString()); // โœ… Calculate total if currentRow is valid and not already finalized if (currentRow.label && currentRow.numbers.length > 0 && currentRow.value > 0) { if (!currentRow.total) { let combinations = 1; if (['TAN', 'FOR', 'QUI'].includes(currentRow.label)) { combinations = currentRow.numbers.length * (currentRow.numbers.length - 1); } else if (['TRE', 'JKP', 'MJP'].includes(currentRow.label)) { combinations = 1; // or your specific logic } else if (currentRow.label === 'BOX') { const n = currentRow.numbers.length; combinations = n > 1 ? (n * (n - 1)) / 2 : 0; } currentRow.total = combinations * currentRow.value * 10; } allRows.push(currentRow); } if (allRows.length === 0) { console.warn("No valid rows to print."); return; } const ticketCount = allRows.reduce((sum, row) => sum + (row.value || 0), 0); const totalAmount = allRows.reduce((sum, row) => sum + (row.total || 0), 0); const now = new Date(); const venue = 'MYS'; const day = String(now.getDate()).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0'); const year = String(now.getFullYear()).slice(-2); const fullYear = now.getFullYear(); const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, ''); const millis = now.getMilliseconds().toString().padStart(3, '0'); // const ticketId = `${venue}/${fullYear}${month}${day}/1`; // For multi-leg pools (TRE, MJP, JKP), show the pool name (trb1, trb2, mjp1, jkp1) instead of race number let ticketId: string; if (['TRE', 'MJP', 'JKP'].includes(this.selectedLabel || '') && this.currentPool) { ticketId = `${venue}/${fullYear}${month}${day}/${this.currentPool}`; } else { ticketId = `${venue}/${fullYear}${month}${day}/${this.selectedRaceNumber}`; } const barcodeId = `1111${day}${month}${year}${timeStr}${millis}`; //----------------------------------------WINLABELS START HERE ------------------------------------------------------ const winLabels = allRows.map(row => { let displayNumbers = row.numbers; let displayLabel = row.label; // Use row.label by default // Override label for TRE to use currentPool (trb1 or trb2) if (row.label === 'TRE' && this.currentPool) { displayLabel = this.currentPool; // Use trb1 or trb2 } // --- Multi-leg pools: Expand 'F' for each leg --- if (['TRE', 'MJP', 'JKP'].includes(row.label)) { // Split by '/' for legs let legs: (number | string)[][] = []; let currentLeg: (number | string)[] = []; for (const n of displayNumbers) { if (n === '/') { legs.push(currentLeg); currentLeg = []; } else { currentLeg.push(n); } } if (currentLeg.length) legs.push(currentLeg); // For each leg, expand 'F' to correct horse numbers for that leg const poolName = row.label === 'MJP' ? 'mjp1' : row.label === 'JKP' ? 'jkp1' : this.currentPool || 'trb1'; const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const poolRaces = raceCardData?.raceVenueRaces?.pools?.[poolName] || []; // Fallback to hardcoded mapping if needed const raceMap: { [key: string]: number[] } = { 'mjp1': [1, 2, 3, 4], 'jkp1': [3, 4, 5, 6, 7], 'trb1': [2, 3, 4], 'trb2': [5, 6, 7] }; const raceIndices: number[] = poolRaces.length > 0 ? poolRaces : (raceMap[poolName] || []); // If no mapping, fallback to consecutive races from 1 const baseRaceIdx = raceIndices.length > 0 ? raceIndices[0] : 1; let expandedLegs: string[] = legs.map((leg, i) => { // Find race index for this leg let raceIdx = raceIndices.length > i ? raceIndices[i] - 1 : (baseRaceIdx - 1 + i); // let expanded = leg.flatMap(n => // n === 'F' // ? this.getHorseNumbersForRaceIdx(raceIdx).map(num => num.toString()) // : [n] // ); let expanded = leg.flatMap((n, idx) => { if (n === 'F') { const horses = this.getHorseNumbersForRaceIdx(raceIdx).map(num => num.toString()).join(','); const isFirst = idx === 0; const isLast = idx === leg.length - 1; if (isFirst && !isLast) return [`${horses}-`]; if (!isFirst && isLast) return [`-${horses}`]; if (isFirst && isLast) return [horses]; // only F in the leg return [`-${horses}-`]; } return [n]; }); // Remove '-' and '#' for display return expanded.filter(n => n !== '-' && n !== '#').join(','); }); // Join legs with '/' let numbersStr = expandedLegs.join(' / '); const label = displayLabel.padEnd(10); const numbers = numbersStr.padEnd(15); const value = (`*${row.value || 0}`).padEnd(8); return `${label}${numbers} ${value}`; } // ๐ŸŽ Expand 'F' to full horse numbers for other pools if (displayNumbers.includes('F')) { // displayNumbers = displayNumbers.flatMap(n => // n === 'F' ? this.getHorseNumbersForSelectedRace().map(num => num.toString()) : [n] // ); displayNumbers = displayNumbers.flatMap((n, idx, arr) => { if (n === 'F') { const horses = this.getHorseNumbersForSelectedRace().map(num => num.toString()).join(','); const isFirst = idx === 0; const isLast = idx === arr.length - 1; if (isFirst && !isLast) return [`${horses}-`]; if (!isFirst && isLast) return [`-${horses}`]; if (isFirst && isLast) return [horses]; // only F return [`-${horses}-`]; } return [n]; }); } let numbersStr = ''; // ๐ŸŽฏ FOR, QUI, TAN logic with box check if (['FOR', 'QUI'].includes(row.label)) { const actualNumbers = displayNumbers.filter(n => n !== '#' && n !== '-').join(','); if (row.numbers.includes('#') || row.isBoxed) { // โœ… box condition numbersStr = `${actualNumbers} - ${actualNumbers}`; } else { numbersStr = actualNumbers; } } else { // ๐Ÿ“ All other pools numbersStr = displayNumbers.filter(n => n !== '#' && n !== '-').join(','); } const label = 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'); //------------------------------------WIN LABELS ENDS HERE -------------------------------- // โœ… Print preview const printData = { ticketId, barcodeId, venue, date: `${day}-${month}-${fullYear}`, winLabels, ticketCount, totalAmount, gstNumber: '29ABCDE1234F2Z5' }; // ๐Ÿงพ Simulated console ticket console.log('--- Simulated Ticket Print ---'); console.log(`Ticket ID : ${printData.ticketId}`); console.log(`Barcode ID : ${printData.barcodeId}`); console.log(`|||||| ||| | ||||| |||| |`); // Dummy barcode console.log(`WIN Labels :`); printData.winLabels.split('\n').forEach(row => console.log(row)); console.log(`*${printData.ticketCount} โ‚น${printData.totalAmount}`); console.log(`GST Number : ${printData.gstNumber}`); console.log(`Date/Time : ${now.toLocaleString()}`); console.log('-----------------------------'); // ๐Ÿ–จ๏ธ Send to printer API const payload = { type: 'ticket', ticketId: printData.ticketId, barcodeId: printData.barcodeId, winLabels: printData.winLabels, ticketCount: printData.ticketCount, totalAmount: printData.totalAmount, gstNumber: printData.gstNumber, dateTime: now.toLocaleString() }; fetch('http://localhost:9100/print', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) // ---------------------sending data to backend --------------------------------- try { const existingTicketsStr = localStorage.getItem('localTickets'); const existingTickets = existingTicketsStr ? JSON.parse(existingTicketsStr) : []; existingTickets.push(payload); localStorage.setItem('localTickets', JSON.stringify(existingTickets)); console.log('๐Ÿ“ฆ [DEBUG] Ticket saved locally to localStorage.'); } catch (error) { console.error('โŒ Failed to store ticket locally:', error); } //-------------------LOG PRINT DETAILS -------------------------------------------------- try { const existingTicketsStr = localStorage.getItem('localTicketsViewlog'); const existingTickets = existingTicketsStr ? JSON.parse(existingTicketsStr) : []; // โ›”๏ธ If there are already 10 tickets, remove the oldest (index 0) if (existingTickets.length >= 10) { existingTickets.shift(); // remove 0th element } // โœ… Push the new ticket existingTickets.push(payload); // ๐Ÿ“ Save updated list back to localStorage localStorage.setItem('localTicketsViewlog', JSON.stringify(existingTickets)); console.log('๐Ÿ“ฆ [DEBUG] Ticket saved locally to localStorage.'); } catch (error) { console.error('โŒ Failed to store ticket locally:', error); } //--------------------------------ENDED HERE ------------------------------------------------ try { localStorage.setItem('localTicketsnew', JSON.stringify([payload])); console.log('๐Ÿ“ฆ [DEBUG] Latest ticket stored in localStorage (previous cleared).'); } catch (error) { console.error('โŒ Failed to store ticket locally:', error); } fetch('http://192.168.1.12:8083/api/tickets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) //----------------------------------------ends here -------------------------- .then(response => { if (!response.ok) { throw new Error(`Printer error: ${response.status}`); } return response.text(); }) .then(result => { console.log("โœ… Print successful:", result); this.erase(); // โœ… Clear selections after successful print }) .catch(error => { console.error("โŒ Print failed:", error); this.erase(); // โœ… Clear selections after successful print }); this.erase(); // โœ… Clear selections after successful print //--------------------Ended Print here ----------------------------- this.selectionService.finalizeCurrentRow(); this.resetSelections(); } //----------------------------------PRINT ENDS HERE ------------------------------------------------------ erase() { this.selectionService.clearSelections(); this.resetSelections(); } resetSelections() { this.selectedLabel = null; this.selectedNumbers = []; this.padValue = ''; this.canPrint = false; this.isBoxed = false; this.totalAmountLimitReached = false; this.showLimitPopup = false; this.tanGroupStage = 0; this.tanGroups = [[], [], []]; this.isFirstGroupComplete = false; this.firstGroup = []; this.secondGroup = []; this.multiLegStage = 0; this.multiLegGroups = [[], [], [], [], []]; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.currentPool = null; this.fieldModalOpen = false; this.fieldInput = ''; this.fieldFEntered = false; this.wspTicketStage = 0; // Clear multi-leg display in Navbar this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null }); } toggleBoxMode() { if (this.totalAmountLimitReached) return; this.isBoxed = !this.isBoxed; const value = parseFloat(this.padValue) || 0; if (this.selectedLabel === 'TAN' && this.isBoxed) { this.tanGroupStage = 0; this.tanGroups = [[], [], []]; this.selectedNumbers = []; } this.selectionService.updatePartial({ isBoxed: this.isBoxed, label: this.selectedLabel || '', numbers: [...this.selectedNumbers], value }); this.updateCanPrint(); } removeLastNumber() { if (!this.selectedLabel || (this.selectedNumbers.length === 0 && !this.selectedNumbers.includes('F'))) return; if (this.selectedNumbers.includes('F') && this.allowedFieldLabels.includes(this.selectedLabel || '')) { this.selectedNumbers = []; this.selectionService.updatePartial({ numbers: [], isBoxed: false, label: this.selectedLabel || '' }); return; } if (this.selectedLabel === 'TAN' && this.isBoxed) { const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[]; if (currentNumbers.length > 0) { currentNumbers.pop(); const groupSize = Math.ceil(currentNumbers.length / 3); const group1 = currentNumbers.slice(0, groupSize); const group2 = currentNumbers.slice(group1.length, group1.length + groupSize); const group3 = currentNumbers.slice(group1.length + group2.length); const combined: (number | string)[] = [...group1]; if (group2.length) combined.push('-', ...group2); if (group3.length) combined.push('-', ...group3); this.selectedNumbers = combined; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers], isBoxed: true, label: 'TAN' }); } return; } if (this.selectedLabel === 'TAN') { const currentGroup = this.tanGroups[this.tanGroupStage]; if (currentGroup.length > 0) { currentGroup.pop(); let combined: (number | string)[] = [...this.tanGroups[0]]; if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]); if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]); this.selectedNumbers = combined; this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); } return; } if (this.multiLegLabels.includes(this.selectedLabel)) { const currentGroup = this.multiLegGroups[this.multiLegStage]; if (currentGroup.length > 0) { currentGroup.pop(); this.updateMultiLegSelection(); } return; } if (this.twoGroupLabels.includes(this.selectedLabel)) { if (!this.isFirstGroupComplete && this.firstGroup.length > 0) { this.firstGroup.pop(); this.selectedNumbers = [...this.firstGroup]; } else if (this.secondGroup.length > 0) { this.secondGroup.pop(); this.selectedNumbers = [...this.firstGroup, '-', ...this.secondGroup]; } this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); return; } this.selectedNumbers.pop(); this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); } private getMaxLegs(poolName: string): number { switch (poolName) { case 'mjp1': return 4; case 'jkp1': return 5; case 'trb1': case 'trb2': case 'TRE': return 3; default: return 5; // Default to 5 for unspecified pools } } private getRaceForLeg(poolName: string, leg: number): number { const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const poolRaces = raceCardData?.raceVenueRaces?.pools?.[poolName] || []; if (poolRaces.length > leg) { return poolRaces[leg]; } // Fallback to default race mapping const raceMap: { [key: string]: number[] } = { 'mjp1': [1, 2, 3, 4], 'jkp1': [3, 4, 5, 6, 7], 'trb1': [2, 3, 4], 'trb2': [5, 6, 7] }; return raceMap[poolName]?.[leg] || (this.multiLegBaseRaceIdx + leg); } private updateLegRaceDisplay(poolName: string) { if (!['mjp1', 'jkp1', 'trb1', 'trb2'].includes(poolName)) { this.currentLegRaceDisplay = ''; this.currentPool = null; return; } const raceIdx = this.getRaceForLeg(poolName, this.multiLegStage); this.currentLegRaceDisplay = `Leg ${this.multiLegStage + 1} (Race ${raceIdx})`; const runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, this.multiLegStage); this.sharedStateService.setRunnerCount(runnerCount); this.sharedStateService.updateSharedData({ type: 'currentLegRace', value: raceIdx }); } private getRunnerCountForLeg(baseIdx: number, leg: number): number { const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const raceIdx = this.getRaceForLeg(this.currentPool || '', leg) - 1; const race = raceCardData?.raceVenueRaces?.races?.[raceIdx] || []; if (race?.runners && Array.isArray(race.runners)) { return race.runners.length; } if (Array.isArray(race)) { return race.length; } return 12; } private handleFieldForSpecialLabels() { if (!this.selectedLabel) return; if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') { if (!this.isFirstGroupComplete) { this.firstGroup = ['F']; this.selectedNumbers = ['F']; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } else { this.secondGroup = ['F']; this.selectedNumbers = [...this.firstGroup, '-', 'F']; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } } else if (this.selectedLabel === 'TAN') { if (this.isBoxed) { this.selectedNumbers = ['F']; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true }); } else { this.tanGroups[this.tanGroupStage] = ['F']; const combined: (number | string)[] = [...this.tanGroups[0]]; if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]); if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]); this.selectedNumbers = combined; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } } else if (this.multiLegLabels.includes(this.selectedLabel)) { this.multiLegGroups[this.multiLegStage] = ['F']; this.updateMultiLegSelection(); this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } } openCalculator() { this.calculatorOpen = true; this.calcDisplay = ''; } closeCalculator() { this.calculatorOpen = false; } press(val: string) { if (this.calcDisplay === 'Error') this.calcDisplay = ''; this.calcDisplay += val; } clearDisplay() { this.calcDisplay = ''; } backspace() { this.calcDisplay = this.calcDisplay === 'Error' ? '' : this.calcDisplay.slice(0, -1); } calculate() { try { this.calcDisplay = eval(this.calcDisplay).toString(); } catch { this.calcDisplay = 'Error'; } } canUseField(): boolean { if (this.totalAmountLimitReached) return false; if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') { if (!this.isFirstGroupComplete && this.firstGroup.length === 0) { return true; } if (this.isFirstGroupComplete && this.secondGroup.length === 0) { return true; } return false; } if (this.selectedLabel === 'TAN' || this.multiLegLabels.includes(this.selectedLabel || '')) { if (this.selectedLabel === 'TAN' && this.tanGroups[this.tanGroupStage].length === 0) { return true; } if (this.multiLegLabels.includes(this.selectedLabel || '') && this.multiLegGroups[this.multiLegStage].length === 0) { return true; } return false; } return ( this.selectedLabel !== null && this.allowedFieldLabels.includes(this.selectedLabel) && this.selectedNumbers.length === 0 ); } openFieldModal() { if (this.totalAmountLimitReached) return; if (['FOR', 'QUI', 'TAN'].includes(this.selectedLabel || '') || this.multiLegLabels.includes(this.selectedLabel || '')) { this.handleFieldForSpecialLabels(); } else { this.fieldModalOpen = true; this.fieldInput = ''; this.fieldFEntered = false; } } closeFieldModal() { this.fieldModalOpen = false; } handleFieldKey(key: string) { if (key === 'BACK') { this.fieldInput = this.fieldInput.slice(0, -1); if (!this.fieldInput.includes('F')) this.fieldFEntered = false; return; } if (key === 'F') { if (!this.fieldFEntered) { this.fieldFEntered = true; this.fieldInput = 'F'; } } else { this.fieldInput += key; } } confirmFieldEntry() { if (!this.fieldFEntered || !this.selectedLabel) return; if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') { if (!this.isFirstGroupComplete) { this.firstGroup = ['F']; this.selectedNumbers = ['F']; } else { this.secondGroup = ['F']; this.selectedNumbers = [...this.firstGroup, '-', 'F']; } this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } else if (this.selectedLabel === 'TAN') { if (this.isBoxed) { this.selectedNumbers = ['F']; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true }); } else { this.tanGroups[this.tanGroupStage] = ['F']; const combined: (number | string)[] = [...this.tanGroups[0]]; if (this.tanGroupStage > 0) combined.push('-', ...this.tanGroups[1]); if (this.tanGroupStage > 1) combined.push('-', ...this.tanGroups[2]); this.selectedNumbers = combined; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } } else if (this.multiLegLabels.includes(this.selectedLabel)) { this.multiLegGroups[this.multiLegStage] = ['F']; this.updateMultiLegSelection(); this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } else { this.selectedNumbers = ['F']; this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: false }); } this.closeFieldModal(); } openPoolReplaceModal() { if (this.totalAmountLimitReached) return; // Determine allowed group based on current selection const groupA = ['WIN', 'SHP', 'THP', 'PLC', 'SHW']; const groupB = ['FOR', 'QUI', 'TAN']; if (groupA.includes(this.selectedLabel || '')) { this.poolReplaceOptions = groupA; } else if (groupB.includes(this.selectedLabel || '')) { this.poolReplaceOptions = groupB; } else { this.poolReplaceOptions = []; } this.poolReplaceOpen = true; } handlePoolReplace(label: string) { this.selectedLabel = label; this.selectedNumbers = []; this.padValue = ''; this.canPrint = false; this.isBoxed = false; this.tanGroupStage = 0; this.tanGroups = [[], [], []]; this.isFirstGroupComplete = false; this.firstGroup = []; this.secondGroup = []; this.multiLegStage = 0; this.multiLegGroups = [[], [], [], [], []]; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.currentPool = null; this.fieldModalOpen = false; this.fieldInput = ''; this.fieldFEntered = false; this.wspTicketStage = 0; this.poolReplaceOpen = false; // Update selection service with new label this.selectionService.updatePartial({ label }); } closePoolReplaceModal() { this.poolReplaceOpen = false; } treButtonClick(btnNum: number) { this.trePopupVisible = false; this._selectTreAfterPopup(btnNum); } private _selectTreAfterPopup(btnNum: number) { this.selectedLabel = 'TRE'; this.selectedNumbers = []; this.padValue = ''; this.canPrint = false; this.isBoxed = false; this.tanGroupStage = 0; this.tanGroups = [[], [], []]; this.isFirstGroupComplete = false; this.firstGroup = []; this.secondGroup = []; this.multiLegStage = 0; this.multiLegGroups = [[], [], [], [], []]; // Map TRE button to specific pool name and base race dynamically const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); const trePoolMap: { [key: number]: { name: string } } = { 1: { name: 'trb1' }, 2: { name: 'trb2' } }; const poolInfo = trePoolMap[btnNum] || { name: 'trb1' }; this.currentPool = poolInfo.name; this.multiLegBaseRaceIdx = this.getBaseRaceIndexForPool(poolInfo.name); // Broadcast TRE selection this.sharedStateService.updateSharedData({ type: 'multiLegPoolStart', value: { label: poolInfo.name, baseRaceIdx: this.multiLegBaseRaceIdx } }); this.updateLegRaceDisplay(poolInfo.name); this.selectionService.updatePartial({ label: 'TRE' }); // --- NEW: Update runners and number pad immediately --- this.actualRunners = this.getActualRunnersForCurrentPoolLeg(); this.runnerCount = this.getRunnerCountForLeg(this.multiLegBaseRaceIdx, 0) || 12; this.numbers = Array.from({ length: 30 }, (_, i) => i + 1); this.numbersFlat = this.numberRows.flat(); } closeTrePopup() { this.trePopupVisible = false; } closeLimitPopup() { this.showLimitPopup = false; } copyNumbers() { if (!this.selectedLabel) { console.warn('Please select a label before copying numbers.'); return; } const selections = this.selectionService.getSelections(); if (selections.length === 0) { console.warn('No previous rows to copy from.'); return; } // Copy numbers from the most recent finalized row const lastRow = selections[selections.length - 1]; const numbersToCopy = [...lastRow.numbers]; // Apply the copied numbers directly this.selectedNumbers = numbersToCopy; // Validate against actual runners const validNumbers = this.selectedNumbers.filter(num => { if (typeof num === 'string') return num === 'F' || num === '-'; return this.actualRunners.has(num); }); this.selectedNumbers = validNumbers; // Update the current row in the selection service this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: this.isBoxed, value: parseFloat(this.padValue) || 0 }); this.updateCanPrint(); } // Add trackByHorse for use in *ngFor trackByHorse(index: number, item: number): number { return item; } // Example usage of _.uniq for enabledHorseNumbers (if you ever set it) setEnabledHorseNumbers(numbers: number[]) { this.enabledHorseNumbers = _.uniq(numbers); } // If you ever need to deduplicate numbers before rendering: get dedupedEnabledHorseNumbers(): number[] { return _.uniq(this.enabledHorseNumbers); } // Update canEnterRow to require value between 1 and 100 get canEnterRow(): boolean { if (this.maxRowsReached) { console.log('[DEBUG] canEnterRow: maxRowsReached is true, disabling Enter'); return false; } // Special handling for WSP if (this.selectedLabel === 'WSP') { const isValidPadValue = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue); const hasNumbers = this.selectedNumbers.length > 0; console.log('[DEBUG] canEnterRow (WSP):', { isValidPadValue, hasNumbers, padValue: this.padValue, selectedNumbers: this.selectedNumbers, wspTicketStage: this.wspTicketStage }); return isValidPadValue && hasNumbers; } // Default logic for non-WSP const currentRow = this.selectionService.getCurrentRow(); const result = !!currentRow.label && !!currentRow.numbers && currentRow.numbers.length > 0 && typeof currentRow.value === 'number' && currentRow.value >= 1 && currentRow.value <= 100 && currentRow.total > 0; console.log('[DEBUG] canEnterRow (non-WSP):', { label: currentRow.label, numbers: currentRow.numbers, value: currentRow.value, total: currentRow.total, result }); return result; } }