From b680150a88349a72e90fc2ed060ceecbbec3b0b0 Mon Sep 17 00:00:00 2001 From: karthik Date: Mon, 8 Sep 2025 15:10:12 +0530 Subject: [PATCH] fix : race card fetching from the server and working --- .../app/components/navbar/navbar.component.ts | 462 +++------- .../components/sidebar/sidebar.component.html | 57 +- .../components/sidebar/sidebar.component.ts | 51 +- .../touch-pad-menu.component.ts | 854 +++++++----------- btc-UI/src/app/home/home.component.ts | 8 +- btc-UI/src/app/login/login.component.ts | 103 +-- btc-UI/src/app/service/btc.service.ts | 244 ++++- docker/docker-compose.yml | 37 +- 8 files changed, 797 insertions(+), 1019 deletions(-) diff --git a/btc-UI/src/app/components/navbar/navbar.component.ts b/btc-UI/src/app/components/navbar/navbar.component.ts index a36b418..397a6d7 100755 --- a/btc-UI/src/app/components/navbar/navbar.component.ts +++ b/btc-UI/src/app/components/navbar/navbar.component.ts @@ -19,20 +19,19 @@ export class NavbarComponent implements OnInit, OnDestroy { private subscription!: Subscription; userName: string = ''; btid: string | null = null; - liveStatusOk: boolean = true; showVenueModal = false; showRaceModal = false; selectedVenue = 'Select Venue'; selectedRace: number = 1; - currentLegRaceDisplay: string = ''; // Display current leg's race or pool start + currentLegRaceDisplay: string = ''; showWalletModal = false; showResultModal = false; showMessagesModal = false; showLogModal = false; - raceCardData: any = {}; + raceCardData: any = null; raceData: any[] = []; objectKeys = Object.keys; @@ -41,37 +40,19 @@ export class NavbarComponent implements OnInit, OnDestroy { multiLegBaseRaceIdx: number = 0; currentPool: string | null = null; - private prevEnabledKey = ''; // For memoization + private prevEnabledKey = ''; - wallet = { - withdraw: 0, - deposit: 0, - payout: 0, - cancel: 0, - ticketing: 0, - balance: 0, - }; + wallet = { withdraw: 0, deposit: 0, payout: 0, cancel: 0, ticketing: 0, balance: 0 }; - logs = [{ - description: '', - venue: '', - ticketNumber: '', - poolName: '', - totalAmount: '', - }]; + logs = [{ description: '', venue: '', ticketNumber: '', poolName: '', totalAmount: '' }]; - messages: string[] = [ - 'System ready.', - 'Please select a venue.', - 'Races updated.', - 'Live status stable.', - ]; + messages: string[] = ['System ready.', 'Please select a venue.', 'Races updated.', 'Live status stable.']; constructor( private btcService: BtcService, private router: Router, private sharedStateService: SharedStateService, - private zone: NgZone // <-- Add NgZone + private zone: NgZone ) {} ngOnInit() { @@ -79,30 +60,19 @@ export class NavbarComponent implements OnInit, OnDestroy { this.btid = localStorage.getItem('btid'); // Use NgZone to run setInterval outside Angular's change detection this.zone.runOutsideAngular(() => { - setInterval(() => { - this.zone.run(() => this.updateDateTime()); - }, 1000); + setInterval(() => { this.zone.run(() => this.updateDateTime()); }, 1000); }); this.subscription = interval(5000) .pipe( switchMap(() => this.btcService.pingLiveStatus().pipe( - catchError((error) => { - console.error('[LIVE STATUS] Ping failed:', error); - this.liveStatusOk = false; - return of(null); - }) + catchError((error) => { console.error('[LIVE STATUS] Ping failed:', error); this.liveStatusOk = false; return of(null); }) )) ).subscribe((response) => { - if (response !== null) { - console.log('[LIVE STATUS] OK'); - this.liveStatusOk = true; - } + if (response !== null) { this.liveStatusOk = true; console.log('[LIVE STATUS] OK'); } }); - - this.sharedStateService.sharedData$.subscribe(data => { if (data.type === 'currentLegRace') { this.selectedRace = data.value; @@ -117,11 +87,11 @@ export class NavbarComponent implements OnInit, OnDestroy { } if (data.type === 'multiLegPoolStart') { const { label, baseRaceIdx } = data.value; - this.currentPool = label; + this.currentPool = this.normalizePoolName(label); this.multiLegBaseRaceIdx = baseRaceIdx; - this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${label}`; + this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${this.currentPool}`; this.updateEnabledHorseNumbersForMultiLeg(baseRaceIdx); - console.log(`[Multi-leg Pool] Selected: ${label}, Base Race: ${baseRaceIdx}`); + console.log(`[Multi-leg Pool] Selected: ${this.currentPool}, Base Race: ${baseRaceIdx}`); } if (data.type === 'multiLegPoolEnd') { this.currentPool = null; @@ -137,115 +107,115 @@ export class NavbarComponent implements OnInit, OnDestroy { this.updateEnabledHorseNumbers(); } }); + + this.loadRaceCardData(); + } + + private safeGetJSON(key: string): any | null { + try { + const raw = localStorage.getItem(key); + if (!raw) return null; + return JSON.parse(raw); + } catch (err) { + console.warn(`[safeGetJSON] failed parse ${key}`, err); + return null; + } + } + + private loadRaceCardData() { + const cached = this.safeGetJSON('rpinfo'); + if (cached && cached.structuredRaceCard) { + this.raceCardData = cached.structuredRaceCard; + this.selectedVenue = this.raceCardData?.venue || 'Select Venue'; + this.updateEnabledHorseNumbers(); + return; + } + const rc = this.safeGetJSON('raceCardData'); + if (rc) { + this.raceCardData = rc; + this.selectedVenue = this.raceCardData?.venue || 'Select Venue'; + this.updateEnabledHorseNumbers(); + return; + } + this.raceCardData = { error: 'Race card not available locally' }; + this.selectedVenue = 'Select Venue'; + } + + private normalizePoolName(name: string | null | undefined): string | null { + if (!name) return null; + const n = String(name).toLowerCase(); + if (n.startsWith('trb') || n.startsWith('tbp')) return n.includes('2') ? 'trb2' : 'trb1'; + if (n.startsWith('mjp')) return n.includes('2') ? 'mjp2' : 'mjp1'; + if (n.startsWith('jkp') || n.startsWith('jpp') || n.startsWith('jk')) return n.includes('2') ? 'jkp2' : 'jkp1'; + return n; } private getLegIndexForRace(poolName: string, race: number): number { - 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]?.indexOf(race) ?? 0; + if (!this.raceCardData) return 0; + const normalized = this.normalizePoolName(poolName) || poolName; + const poolMap: { [key: string]: number[] } = this.raceCardData?.pools || {}; + const arr = poolMap[normalized] || poolMap[poolName] || []; + return arr.indexOf(race) >= 0 ? arr.indexOf(race) : 0; } private updateEnabledHorseNumbersForMultiLeg(baseRaceIdx: number) { - const raceCardData = this.raceCardData?.raceVenueRaces?.races || []; + const racesArr = this.raceCardData?.raceVenueRaces?.races || []; const legCount = this.getLegCountForLabel(); - const key = `${this.currentPool}-${baseRaceIdx}-${legCount}`; - - if (this.prevEnabledKey === key) return; // 🧠 Memoization + const poolKey = this.currentPool || ''; + const key = `${poolKey}-${baseRaceIdx}-${legCount}`; + if (this.prevEnabledKey === key) return; this.prevEnabledKey = key; let combinedHorseNumbers: number[] = []; - - const raceIndices = Array.from({ length: legCount }, (_, i) => - this.getRaceForLeg(this.currentPool || '', i) - 1 - ); + const raceIndices = Array.from({ length: legCount }, (_, i) => this.getRaceForLeg(poolKey, i) - 1); for (const raceIdx of raceIndices) { - const race = raceCardData[raceIdx] || []; - if (Array.isArray(race)) { - const horses = race - .map((runner: any) => runner?.horseNumber) - .filter((n: number) => typeof n === 'number' && n >= 1 && n <= 30); + const race = racesArr[raceIdx] || {}; + if (Array.isArray(race.horses)) { + const horses = race.horses.filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30); combinedHorseNumbers.push(...horses); } } this.enabledHorseNumbers = Array.from(new Set(combinedHorseNumbers)); - - this.sharedStateService.updateSharedData({ - type: 'enabledHorseNumbers', - value: this.enabledHorseNumbers, - }); - + this.sharedStateService.updateSharedData({ type: 'enabledHorseNumbers', value: this.enabledHorseNumbers }); console.log('[Multi-leg Pool] Updated enabled horse numbers:', this.enabledHorseNumbers); } private getLegCountForLabel(): number { - switch (this.currentPool) { - case 'mjp1': return 4; - case 'jkp1': return 5; - case 'trb1': - case 'trb2': return 3; + const p = (this.currentPool || '').toLowerCase(); + switch (p) { + case 'mjp1': case 'mjp2': return 4; + case 'jkp1': case 'jkp2': return 5; + case 'trb1': case 'trb2': return 3; default: return 3; } } private getRaceForLeg(poolName: string, leg: number): number { - 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); + const normalized = this.normalizePoolName(poolName) || poolName; + const poolMap: { [key: string]: number[] } = this.raceCardData?.pools || {}; + const arr = poolMap[normalized] || poolMap[poolName] || []; + if (arr.length > leg) return arr[leg]; + return (this.multiLegBaseRaceIdx || 1) + leg; } - updateDateTime() { - const now = new Date(); - this.dateTime = now.toLocaleString(); - } + updateDateTime() { this.dateTime = new Date().toLocaleString(); } @HostListener('window:resize', ['$event']) onResize(event: any) { this.screenWidth = event.target.innerWidth; - if (this.screenWidth > 800) { - this.isMenuOpen = false; - } + if (this.screenWidth > 800) this.isMenuOpen = false; } - toggleMenu() { - this.isMenuOpen = !this.isMenuOpen; - } + toggleMenu() { this.isMenuOpen = !this.isMenuOpen; } - openVenueModal() { - - const cachedData = localStorage.getItem('raceCardData'); - if (cachedData) { - this.raceCardData = JSON.parse(cachedData); - // console.log('πŸ“¦ Loaded race card from localStorage:', this.raceCardData); // comment out for perf - this.selectedVenue = this.raceCardData?.Venue || 'Select Venue'; - this.updateEnabledHorseNumbers(); - } else { - this.raceCardData = { raceVenueRaces: { races: [] }, pools: {} }; - // console.warn('⚠️ No race card data found in localStorage.'); // comment out for perf - } - console.log('[MODAL] Opening venue modal'); - this.showVenueModal = true; - } + openVenueModal() { this.loadRaceCardData(); this.showVenueModal = true; } openRaceModal() { - console.log('[MODAL] Opening race modal'); this.showRaceModal = true; - - const venueIndex = Object.keys(this.raceCardData?.raceVenueRaces?.races || []) - .findIndex((_, idx) => idx === this.selectedRaceId); - - if (venueIndex !== -1) { - this.raceData = this.raceCardData.raceVenueRaces.races[venueIndex] || []; - } + const race = this.raceCardData?.raceVenueRaces?.races?.[this.selectedRaceId]; + this.raceData = race ? (race.horses || []) : []; } closeModals() { @@ -259,16 +229,9 @@ export class NavbarComponent implements OnInit, OnDestroy { } selectVenue(index: number) { - const venue = this.raceCardData?.Venue || 'Unknown Venue'; - this.selectedVenue = venue; + this.selectedVenue = this.raceCardData?.venue || 'Unknown Venue'; this.selectedRaceId = index; - - this.sharedStateService.updateSharedData({ - type: 'selectedVenue', - value: this.selectedVenue, - }); - - console.log('[VENUE] Venue resolved to:', this.selectedVenue); + this.sharedStateService.updateSharedData({ type: 'selectedVenue', value: this.selectedVenue }); this.closeModals(); } @@ -277,244 +240,103 @@ export class NavbarComponent implements OnInit, OnDestroy { this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; + this.sharedStateService.updateSharedData({ type: 'selectedRace', value: this.selectedRace }); - this.sharedStateService.updateSharedData({ - type: 'selectedRace', - value: this.selectedRace, - }); - - const raceCard = JSON.parse(localStorage.getItem('raceCardData') || '{}'); - const raceList = raceCard?.raceVenueRaces?.races || []; - const selectedRaceData = raceList[race - 1] || []; - const runnerCount = selectedRaceData.length || 12; - + const raceData = this.raceCardData?.raceVenueRaces?.races?.[race - 1] || {}; + const runnerCount = Array.isArray(raceData.horses) ? raceData.horses.length : (raceData.runners?.length || 12); this.sharedStateService.setRunnerCount(runnerCount); this.updateEnabledHorseNumbers(); - - console.log('[RACE] Race selected:', this.selectedRace, '| Runner count:', runnerCount); this.closeModals(); } updateEnabledHorseNumbers() { const raceIndex = this.selectedRace - 1; - const race = this.raceCardData?.raceVenueRaces?.races?.[raceIndex]; + const race = this.raceCardData?.raceVenueRaces?.races?.[raceIndex] || {}; + let horses: any[] = []; - if (Array.isArray(race)) { - this.enabledHorseNumbers = race - .map((runner: any) => runner?.horseNumber) - .filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30); - } else { - this.enabledHorseNumbers = []; - } - - this.sharedStateService.updateSharedData({ - type: 'enabledHorseNumbers', - value: this.enabledHorseNumbers, - }); + if (Array.isArray(race?.horses)) horses = race.horses; + else if (Array.isArray(race?.runners)) horses = race.runners.map((r: any) => r.number ?? r); + else if (Array.isArray(race)) horses = race; + this.enabledHorseNumbers = horses.map((n: any) => Number(n)).filter((n: number) => !Number.isNaN(n) && n >= 1 && n <= 30); + this.sharedStateService.updateSharedData({ type: 'enabledHorseNumbers', value: this.enabledHorseNumbers }); console.log('[HORSE NUMBERS] Enabled horse numbers:', this.enabledHorseNumbers); } - selectHorseNumber(number: number) { - console.log('[HORSE] Selected horse number:', number); - } + selectHorseNumber(number: number) { console.log('[HORSE] Selected horse number:', number); } - openWalletModal() { - console.log('[MODAL] Opening wallet modal'); - this.showWalletModal = true; - } + openWalletModal() { this.showWalletModal = true; } + openResultModal() { this.showResultModal = true; } + openMessagesModal() { this.showMessagesModal = true; } + openLogModal() { this.showLogModal = true; } - openResultModal() { - console.log('[MODAL] Opening result modal'); - this.showResultModal = true; - } + formattedTicketLogs: any[] = []; - openMessagesModal() { - console.log('[MODAL] Opening messages modal'); - this.showMessagesModal = true; - } - - openLogModal() { - console.log('[MODAL] Opening log modal'); - this.showLogModal = true; - } - - -// formattedTicketLogs: { -// label: string; -// numbers: number[]; -// count: number; -// amount: number; -// }[] = []; -formattedTicketLogs: { - pool: string; - horses: string; - horsesArray: string[][]; - ticketCountLabel: string; - price: string; - numbers: number[]; - count: number; - amount: number; - maskedBarcode: string; - displayBarcode: string; -}[] = []; - -openViewLog() { - const storedTickets = localStorage.getItem('localTicketsViewlog'); - - if (storedTickets) { + openViewLog() { + const storedTickets = localStorage.getItem('localTicketsViewlog'); + if (!storedTickets) { this.formattedTicketLogs = []; this.showLogModal = true; return; } const tickets = JSON.parse(storedTickets); - - this.formattedTicketLogs = tickets.map((ticket: any, index: number) => { + this.formattedTicketLogs = tickets.map((ticket: any) => { const rawLabel = ticket.winLabels?.trim() || ''; - const numbers = ticket.numbers || []; - const count = ticket.ticketCount || 0; - const amount = ticket.totalAmount || 0; - const barcodeId = ticket.barcodeId || ''; - let pool = '', horses = '', horsesArray: string[][] = [], ticketCountLabel = '', price = '', maskedBarcode = '' , displayBarcode = ''; + const barcodeId = ticket.barcodeId || ''; + let pool = '', horses = '', horsesArray: string[][] = [], ticketCountLabel = '', price = '', maskedBarcode = '', displayBarcode = ''; if (rawLabel) { - // 1️⃣ Extract pool (first word) const parts = rawLabel.split(/\s+/); pool = parts[0]; - - // 2️⃣ Extract ticket count (*n) & price if exists const countMatch = rawLabel.match(/\*(\d+)(?:\s+(Rs\s*\d+))?/); - if (countMatch) { - ticketCountLabel = `*${countMatch[1]}`; - price = countMatch[2] || ''; - } - - // 3️⃣ Extract horses part (between pool name & ticket count) + if (countMatch) { ticketCountLabel = `*${countMatch[1]}`; price = countMatch[2] || ''; } const horsesPartMatch = rawLabel.match(/^\w+\s+(.+?)\s+\*\d+/); if (horsesPartMatch) { horses = horsesPartMatch[1].trim(); - - // Special pools split into races - if (['MJP', 'JKP', 'TRE'].includes(pool)) { - horsesArray = horses.split('/').map(r => - r.trim().split(',').map(h => h.trim()) - ); + if (['MJP', 'JKP', 'TRE', 'trb1', 'trb2', 'mjp1', 'jkp1'].includes(pool)) { + horsesArray = horses.split('/').map((r: string) => r.trim().split(',').map(h => h.trim())); } else { horsesArray = [horses.split(',').map(h => h.trim())]; } } } - if (barcodeId) { - const last4 = barcodeId.slice(-4); + if (barcodeId) { + const last4 = barcodeId.slice(-4); + maskedBarcode = btoa(barcodeId.slice(0, -4)); + displayBarcode = '********' + last4; + } - // Encrypt everything except last4 - const encryptedPart = btoa(barcodeId.slice(0, -4)); - - // Store masked barcode (if you still need encrypted form) - maskedBarcode = encryptedPart; - - // For GUI β†’ show ******** + last4 - displayBarcode = '********' + last4; -} - - - console.log(maskedBarcode); - console.log('Decoded:', atob(maskedBarcode)); - - - return { - pool, - horses, - horsesArray, - ticketCountLabel, - price, - numbers, - count, - amount, - maskedBarcode, - displayBarcode - }; + return { pool, horses, horsesArray, ticketCountLabel, price, numbers: ticket.numbers || [], count: ticket.ticketCount || 0, amount: ticket.totalAmount || 0, maskedBarcode, displayBarcode }; }); - } else { - console.log('No tickets found in localStorage.'); - this.formattedTicketLogs = []; + + this.showLogModal = true; } - this.showLogModal = true; - console.log('Log modal opened. Final formattedTicketLogs:', this.formattedTicketLogs); -} + logout(): void { + const name = localStorage.getItem('userName') || 'Unknown User'; + const employeeId = localStorage.getItem('employeeId') || '000000'; + const printData = { name, employeeId, action: 'logout', type: 'logout' }; + console.log('[LOGOUT] Initiating logout with printData:', printData); -logout(): void { - const name = localStorage.getItem('userName') || 'Unknown User'; - const employeeId = localStorage.getItem('employeeId') || '000000'; - - const printData = { - name, - employeeId, - action: 'logout', - type: 'logout' // πŸ‘ˆ This is the missing piece - }; - - console.log('[LOGOUT] Initiating logout with printData:', printData); - - fetch('http://localhost:9100/print', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(printData), - }) - .then((res) => { - if (!res.ok) throw new Error('Logout print failed'); - console.log('[LOGOUT] Print successful'); - (window as any).electronAPI?.closeSecondScreen?.(); - localStorage.clear(); - this.router.navigate(['/logout']); + fetch('http://localhost:9100/print', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(printData), }) - .catch((err) => { - console.error('[LOGOUT] Error printing:', err); - (window as any).electronAPI?.closeSecondScreen?.(); - localStorage.clear(); - this.router.navigate(['/logout']); - }); -} - - - - - - // logout(): void { - // const name = localStorage.getItem('userName') || 'Unknown User'; - // const employeeId = localStorage.getItem('employeeId') || '000000'; - - // const printData = { name, employeeId, action: 'logout' }; - - // console.log('[LOGOUT] Initiating logout with printData:', printData); - - // fetch('http://localhost:9100/print', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(printData), - // }) - // .then((res) => { - // if (!res.ok) throw new Error('Logout print failed'); - // console.log('[LOGOUT] Print successful'); - // (window as any).electronAPI?.closeSecondScreen?.(); - // localStorage.clear(); - // this.router.navigate(['/logout']); - // }) - // .catch((err) => { - // console.error('[LOGOUT] Error printing:', err); - // (window as any).electronAPI?.closeSecondScreen?.(); - // localStorage.clear(); - // this.router.navigate(['/logout']); - // }); - // } + .then((res) => { + if (!res.ok) throw new Error('Logout print failed'); + console.log('[LOGOUT] Print successful'); + (window as any).electronAPI?.closeSecondScreen?.(); + localStorage.clear(); + this.router.navigate(['/logout']); + }) + .catch((err) => { + console.error('[LOGOUT] Error printing:', err); + (window as any).electronAPI?.closeSecondScreen?.(); + localStorage.clear(); + this.router.navigate(['/logout']); + }); + } ngOnDestroy(): void { - if (this.subscription) { - this.subscription.unsubscribe(); - } + if (this.subscription) this.subscription.unsubscribe(); } - // Add trackByHorse for use in *ngFor - trackByHorse(index: number, item: number): number { - return item; - } + trackByHorse(index: number, item: number): number { return item; } } diff --git a/btc-UI/src/app/components/sidebar/sidebar.component.html b/btc-UI/src/app/components/sidebar/sidebar.component.html index 6a9e745..2023479 100755 --- a/btc-UI/src/app/components/sidebar/sidebar.component.html +++ b/btc-UI/src/app/components/sidebar/sidebar.component.html @@ -221,7 +221,7 @@ -
+ +
+
+

VIEW RC

+
+ +

πŸ“ Venue: {{ raceCardData.venue }}

+

πŸ“… Date: {{ raceCardData.date }}

+ +
+

πŸ‡ Race Lists

+ + + + + + + + + + + + + +
RacesRace Numbers
Race {{ race.raceNo }}{{ race.horses.join(', ') }}
+
+ +
+

🎯Races

+ + + + + + + + + + + + + +
PoolRaces
{{ pool }}{{ raceCardData.pools.comboRaces[pool].join(', ') }}
+
+
+ + +

❌ {{ raceCardData?.error }}

+
+
+ diff --git a/btc-UI/src/app/components/sidebar/sidebar.component.ts b/btc-UI/src/app/components/sidebar/sidebar.component.ts index 935b550..d66f686 100755 --- a/btc-UI/src/app/components/sidebar/sidebar.component.ts +++ b/btc-UI/src/app/components/sidebar/sidebar.component.ts @@ -441,17 +441,52 @@ ${receiptText} raceCardData: any = null; // βœ… Hold fetched data - constructor( - private btcService: BtcService + +// openViewRcPopup() { +// const cachedData = localStorage.getItem('rpinfo'); - ) {} +// if (cachedData) { +// this.raceCardData = JSON.parse(cachedData); +// console.log('πŸ“¦ Loaded race card from localStorage:', this.raceCardData); +// } else { +// this.raceCardData = { error: 'Race card not available locally' }; +// console.warn('⚠️ No race card data found in localStorage.'); +// } - openViewRcPopup() { - const cachedData = localStorage.getItem('raceCardData'); +// this.showViewRc = true; +// } + +// openViewRcPopup() { +// const cachedData = localStorage.getItem('rpinfo'); + +// if (cachedData) { +// try { +// this.raceCardData = JSON.parse(cachedData); // now it's an array +// console.log('πŸ“¦ Loaded race card from localStorage:', this.raceCardData); +// } catch (e) { +// console.error('Error parsing rpinfo:', e); +// this.raceCardData = { error: 'Invalid race card data' }; +// } +// } else { +// this.raceCardData = { error: 'Race card not available locally' }; +// console.warn('⚠️ No race card data found in localStorage.'); +// } + +// this.showViewRc = true; +// } + +openViewRcPopup() { + const cachedData = localStorage.getItem('rpinfo'); if (cachedData) { - this.raceCardData = JSON.parse(cachedData); - console.log('πŸ“¦ Loaded race card from localStorage:', this.raceCardData); + try { + const parsed = JSON.parse(cachedData); + this.raceCardData = parsed.structuredRaceCard; // βœ… only keep structured + console.log('πŸ“¦ Loaded structured race card:', this.raceCardData); + } catch (e) { + console.error('Error parsing rpinfo:', e); + this.raceCardData = { error: 'Invalid race card data' }; + } } else { this.raceCardData = { error: 'Race card not available locally' }; console.warn('⚠️ No race card data found in localStorage.'); @@ -460,6 +495,8 @@ ${receiptText} this.showViewRc = true; } + + objectKeys = Object.keys; closeViewRcPopup() { diff --git a/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts b/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts index 4b97aba..7a105cf 100755 --- a/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts +++ b/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; imports: [CommonModule], templateUrl: './touch-pad-menu.component.html', styleUrls: ['./touch-pad-menu.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush // πŸ”₯ CRUCIAL for performance + changeDetection: ChangeDetectionStrategy.OnPush }) export class TouchPadMenuComponent implements OnInit, OnDestroy { @Input() ticketingActive: boolean = false; @@ -65,9 +65,9 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { // Multi-leg logic (TRE, MJP, JKP) multiLegStage = 0; multiLegGroups: (number | string)[][] = [[], [], [], [], []]; - multiLegBaseRaceIdx: number = 0; // Track starting race index + multiLegBaseRaceIdx: number = 0; // Track starting race index (1-based) currentLegRaceDisplay: string = ''; // Display current leg's race - currentPool: string | null = null; // Track current pool (mjp1, jkp1, trb1, trb2) + currentPool: string | null = null; // canonical pool key like 'mjp1','trb1','jkp1' isBoxed: boolean = false; @@ -94,24 +94,41 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { btid: string | null = null; raceCardData: any = {}; + structuredRaceCard: any = {}; + + selectedRaceNumber: string = '1'; + selectionService: SelectionService; + sharedStateService: SharedStateService; + labelRestrictionService: LabelRestrictionService; constructor( - private selectionService: SelectionService, - private sharedStateService: SharedStateService, - private labelRestrictionService: LabelRestrictionService, - private ngZone: NgZone // <-- inject NgZone - ) {} - - - selectedRaceNumber: string = '1'; // Default + selectionService: SelectionService, + sharedStateService: SharedStateService, + labelRestrictionService: LabelRestrictionService, + private ngZone: NgZone + ) { + this.selectionService = selectionService; + this.sharedStateService = sharedStateService; + this.labelRestrictionService = labelRestrictionService; + } ngOnInit() { - this.runnerCountSubscription = this.sharedStateService.runnerCount$.subscribe(count => { + // Prefer rpinfo.structuredRaceCard + const rpinfo = this.safeGetJSON('rpinfo'); + if (rpinfo && rpinfo.structuredRaceCard) { + this.structuredRaceCard = rpinfo.structuredRaceCard; + } else { + // fallback + const rc = this.safeGetJSON('raceCardData'); + this.structuredRaceCard = (rc && rc.structuredRaceCard) ? rc.structuredRaceCard : (rc || {}); + } + this.raceCardData = this.structuredRaceCard; + + 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'); // --- NEW: Update actualRunners when runner count changes --- this.setActualRunners(); @@ -119,10 +136,10 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.labelRowsFlat = this.labelRows.flat(); - this.selectionsSubscription = this.selectionService.selections$.subscribe(selections => { + this.selectionsSubscription = this.selectionService.selections$.subscribe((selections: SelectionData[]) => { this.currentSelections = selections; this.maxRowsReached = selections.length >= 5; - const totalAmount = selections.reduce((sum, selection) => sum + selection.total, 0); + const totalAmount = selections.reduce((sum: number, selection: SelectionData) => sum + (selection.total || 0), 0); this.totalAmountLimitReached = totalAmount >= 5000; this.blockedLabels = this.labelRestrictionService.getBlockedLabels(selections); if (!this.totalAmountLimitReached) { @@ -130,33 +147,30 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } }); - this.currentRowSubscription = this.selectionService.currentRow$.subscribe(row => { - this.currentTotal = row.total; + this.currentRowSubscription = this.selectionService.currentRow$.subscribe((row: SelectionData) => { + this.currentTotal = row.total || 0; }); - // --- 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(); - } -}); + // Subscribe to selectedRace (from navbar) + this.sharedStateService.selectedRace$.subscribe((race: number) => { + this.selectedRaceNumber = String(race || '1'); + if (this.currentPool && this.multiLegLabels.includes(this.selectedLabel || '')) { + // multi leg currently active: update display & runners to reflect pool mapping + 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(); + } + }); + // legacy storage - keep for compatibility if rpinfo absent const data = localStorage.getItem('raceCardData'); if (data) { - this.raceCardData = JSON.parse(data); - } else { - this.raceCardData = {}; + try { this.raceCardData = JSON.parse(data); } catch { this.raceCardData = this.raceCardData || {}; } } } @@ -166,16 +180,45 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.runnerCountSubscription?.unsubscribe(); } + private safeGetJSON(key: string): any | null { + try { + const raw = localStorage.getItem(key); + if (!raw) return null; + return JSON.parse(raw); + } catch { + return null; + } + } + + // Normalize pool name to canonical keys used in structuredRaceCard + private normalizePoolName(name: string | null | undefined): string | null { + if (!name) return null; + const n = String(name).toLowerCase(); + + if (n.startsWith('trb') || n.startsWith('tbp')) { + return n.includes('2') ? 'trb2' : 'trb1'; + } + if (n.startsWith('mjp')) { + return n.includes('2') ? 'mjp2' : 'mjp1'; + } + if (n.startsWith('jkp') || n.startsWith('jpp') || n.startsWith('jkp')) { + return n.includes('2') ? 'jkp2' : 'jkp1'; + } + // default: return lowercase + return n; + } + // --- 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))); + const races = this.structuredRaceCard?.raceVenueRaces?.races || []; + const poolName = this.normalizePoolName(this.currentPool) || ''; + const poolRaces: number[] = this.structuredRaceCard?.pools?.[poolName] || []; + const raceIdx = poolRaces.length > this.multiLegStage + ? poolRaces[this.multiLegStage] - 1 + : (this.multiLegBaseRaceIdx - 1) + this.multiLegStage; + const race = races[raceIdx]; + if (race?.horses && Array.isArray(race.horses)) { + return new Set(race.horses.map((num: any) => Number(num))); } return new Set(); } @@ -191,14 +234,11 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { // --- 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))); + const races = this.structuredRaceCard?.raceVenueRaces?.races || []; + const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10) - 1; + const race = races[selectedRaceIdx]; + if (race?.horses && Array.isArray(race.horses)) { + return new Set(race.horses.map((num: any) => Number(num))); } return new Set(); } @@ -216,18 +256,18 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } get showShashEnter(): boolean { - const label = this.selectedLabel || ''; - if (['FOR', 'QUI', 'TAN'].includes(label) && this.isBoxed) { - return false; + 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); } - 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') { @@ -268,7 +308,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { get isWSPSelection(): boolean { const currentRow = this.selectionService.getCurrentRow(); - return ['WIN', 'SHP', 'PLC'].includes(currentRow.label) || + return ['WIN', 'SHP', 'PLC'].includes(currentRow.label) || this.selectionService.getSelections().some(sel => ['WIN', 'SHP', 'PLC'].includes(sel.label)); } @@ -279,44 +319,41 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } isLabelDisabled(label: string): boolean { - return this.disabledLabels.includes(label) || - this.totalAmountLimitReached || + 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) { + // 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 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); } - // 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; @@ -326,6 +363,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.trePopupVisible = true; return; } + this.selectedLabel = label; this.selectedNumbers = []; this.padValue = ''; @@ -334,13 +372,14 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { // 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; + // Map to canonical pool key + const poolName = label === 'MJP' ? 'mjp1' : label === 'JKP' ? 'jkp1' : 'trb1'; this.currentPool = poolName; - this.multiLegBaseRaceIdx = this.getBaseRaceIndexForPool(poolName); + this.multiLegBaseRaceIdx = this.getBaseRaceIndexForPool(poolName); // Broadcast race and pool info for navbar // Broadcast race and pool info for navbar - this.sharedStateService.updateSharedData({ - type: 'multiLegPoolStart', - value: { label: poolName, baseRaceIdx: this.multiLegBaseRaceIdx } + this.sharedStateService.updateSharedData({ + type: 'multiLegPoolStart', + value: { label: poolName, baseRaceIdx: this.multiLegBaseRaceIdx } }); this.updateLegRaceDisplay(poolName); // --- NEW: Update runners and number pad immediately --- @@ -368,7 +407,6 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { 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, @@ -386,34 +424,29 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.selectionService.updatePartial({ label: '', numbers: [], value: 0, total: 0 }); return; } - //----------------------------------ended here---------------------------------------------------- - // Reset TAN + // Reset group states 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 races = this.structuredRaceCard?.raceVenueRaces?.races || []; + const totalRaces = 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 + const poolKey = this.normalizePoolName(poolName) || poolName; + const poolRaces: number[] = this.structuredRaceCard?.pools?.[poolKey] || []; + let baseRaceIdx = poolRaces.length > 0 ? poolRaces[0] : this.getDefaultBaseRace(poolKey); if (baseRaceIdx + maxLegs - 1 > totalRaces) { baseRaceIdx = Math.max(1, totalRaces - maxLegs + 1); } @@ -423,18 +456,17 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { 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 + 'mjp1': 3, + 'jkp1': 2, + 'trb1': 4 }; - return poolRaceMap[poolName] || this.sharedStateService.getSelectedRace(); + return poolRaceMap[poolName.toLowerCase()] || parseInt(this.selectedRaceNumber, 10); } selectNumber(number: number) { if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return; - // TAN Box mode + // TAN boxed if (this.selectedLabel === 'TAN' && this.isBoxed) { if (!this.selectedNumbers.includes(number)) { const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[]; @@ -469,8 +501,8 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { return; } - // Multi-leg logic (TRE, MJP, JKP) - if (this.multiLegLabels.includes(this.selectedLabel)) { + // Multi-leg + if (this.multiLegLabels.includes(this.selectedLabel || '')) { if (!this.multiLegGroups[this.multiLegStage].includes(number)) { this.multiLegGroups[this.multiLegStage].push(number); this.updateMultiLegSelection(); @@ -512,30 +544,27 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) { return; } - - // Default single-number selection (WIN, SHP, THP, etc.) - if (!this.selectedNumbers.includes(number)) { - this.selectedNumbers.push(number); - this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); - + // 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 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); + + 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, numbers: newNumbers }; - } - return sel; - }); - this.selectionService.setSelections(updated); + return sel; + }); + this.selectionService.setSelections(updated); + } } } - } private updateMultiLegSelection() { const combined: (number | string)[] = []; @@ -590,15 +619,10 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) { 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, value: 0, total: 0 }; } return sel; }); @@ -609,63 +633,52 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) { } onPadEnter() { - if (this.maxRowsReached) return; + 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]; + } + + 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; + } - 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' && this.isBoxed) return; if (this.selectedLabel === 'TAN') { if (this.tanGroupStage < 2 && this.tanGroups[this.tanGroupStage].length > 0) { @@ -700,9 +713,9 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) { } } } - //-----------------------------ENTER PAD VALUE------------------------------------- -enterPadVal(key: string) { + + enterPadVal(key: string) { if (!this.numericPadEnabled || this.totalAmountLimitReached) return; if (key === 'X') { @@ -735,15 +748,10 @@ enterPadVal(key: string) { 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, value, total }; } return sel; }); @@ -860,14 +868,9 @@ enterPadVal(key: string) { this.updateCanPrint(); } } -//--------------------------------------------------------------------------------------------- updateCanPrint() { - // Disable Enter if maxRowsReached - if (this.maxRowsReached) { - this.canPrint = false; - return; - } + 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 || ''); @@ -880,23 +883,15 @@ enterPadVal(key: string) { // 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') { + 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 + 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 @@ -907,7 +902,7 @@ enterPadVal(key: string) { } print() { - const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + sel.total, 0); + 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 || ''); @@ -918,20 +913,14 @@ enterPadVal(key: string) { return group.length; }).slice(0, maxLegs); const units = parseFloat(this.padValue) || 0; - currentRowAmount = this.calculateMultiLegAmount( - this.selectedLabel as 'TRE' | 'MJP' | 'JKP', - horsesPerLeg, - units - ); + 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; - } + if (horsesPerLeg.some(count => count === 0)) return; } else { currentRowAmount = this.currentTotal; if (selectionsTotal + currentRowAmount > 5000) { @@ -940,96 +929,46 @@ enterPadVal(key: string) { 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); + // Helpers: get horse numbers for selected race and specific race index + getHorseNumbersForSelectedRace(): number[] { + const races = this.structuredRaceCard?.raceVenueRaces?.races || []; + const selectedRaceIdx = parseInt(this.selectedRaceNumber, 10) - 1; + const race = races[selectedRaceIdx]; + if (race?.horses && Array.isArray(race.horses)) return race.horses.map((n: any) => Number(n)); 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 || []; + getHorseNumbersForRaceIdx(raceIdx: number): number[] { + const races = this.structuredRaceCard?.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 { + if (race?.horses && Array.isArray(race.horses)) return race.horses.map((n: any) => Number(n)); return []; } -} -clearLocalTickets() { - localStorage.removeItem('localTickets'); - console.log('🧹 localTickets cleared from localStorage'); + clearLocalTickets() { + localStorage.removeItem('localTickets'); + console.log('🧹 localTickets cleared from localStorage'); // Clear the print count - localStorage.removeItem('printClickCount'); - console.log('🧼 printClickCount cleared from localStorage'); + 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'); -} + this.sharedStateService.setSalesTotal(0); + this.sharedStateService.setReceiveTotal(0); + this.selectionService.clearSelections(); + this.resetSelections(); + } //-------------------PRINT LOGIC---------------------------------------- @@ -1389,12 +1328,9 @@ try { this.fieldInput = ''; this.fieldFEntered = false; this.wspTicketStage = 0; + // Clear multi-leg display in Navbar - // Clear multi-leg display in Navbar - this.sharedStateService.updateSharedData({ - type: 'multiLegPoolEnd', - value: null - }); + this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null }); } toggleBoxMode() { @@ -1420,11 +1356,7 @@ try { if (this.selectedNumbers.includes('F') && this.allowedFieldLabels.includes(this.selectedLabel || '')) { this.selectedNumbers = []; - this.selectionService.updatePartial({ - numbers: [], - isBoxed: false, - label: this.selectedLabel || '' - }); + this.selectionService.updatePartial({ numbers: [], isBoxed: false, label: this.selectedLabel || '' }); return; } @@ -1440,11 +1372,7 @@ try { 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' - }); + this.selectionService.updatePartial({ numbers: [...this.selectedNumbers], isBoxed: true, label: 'TAN' }); } return; } @@ -1488,62 +1416,43 @@ try { } 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 + const p = (this.normalizePoolName(poolName) || '').toLowerCase(); + switch (p) { + case 'mjp1': case 'mjp2': return 4; + case 'jkp1': case 'jkp2': return 5; + case 'trb1': case 'trb2': return 3; + default: return 3; } } 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); + const poolKey = this.normalizePoolName(poolName) || poolName || ''; + const poolRaces: number[] = this.structuredRaceCard?.pools?.[poolKey] || []; + if (poolRaces.length > leg) return poolRaces[leg]; + const comboRaces: number[] = this.structuredRaceCard?.comboRaces?.[poolKey] || []; + if (comboRaces.length > leg) return comboRaces[leg]; + return (this.multiLegBaseRaceIdx || 1) + leg; } private updateLegRaceDisplay(poolName: string) { - if (!['mjp1', 'jkp1', 'trb1', 'trb2'].includes(poolName)) { + const poolKey = this.normalizePoolName(poolName) || ''; + if (!['mjp1', 'jkp1', 'trb1', 'trb2', 'mjp2', 'jkp2'].includes(poolKey)) { this.currentLegRaceDisplay = ''; this.currentPool = null; return; } - const raceIdx = this.getRaceForLeg(poolName, this.multiLegStage); + const raceIdx = this.getRaceForLeg(poolKey, 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 - }); + this.sharedStateService.updateSharedData({ type: 'currentLegRace', value: raceIdx }); } private getRunnerCountForLeg(baseIdx: number, leg: number): number { - const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); + const races = this.structuredRaceCard?.raceVenueRaces?.races || []; 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; - } + const race = races[raceIdx]; + if (race?.horses && Array.isArray(race.horses)) return race.horses.length; return 12; } @@ -1554,48 +1463,28 @@ try { if (!this.isFirstGroupComplete) { this.firstGroup = ['F']; this.selectedNumbers = ['F']; - this.selectionService.updatePartial({ - label: this.selectedLabel, - numbers: [...this.selectedNumbers], - isBoxed: false - }); + 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 - }); + 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 - }); + 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.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 - }); + this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); } } @@ -1612,28 +1501,16 @@ try { 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; - } + 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; - } + 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 - ); + return this.selectedLabel !== null && this.allowedFieldLabels.includes(this.selectedLabel) && this.selectedNumbers.length === 0; } openFieldModal() { @@ -1647,9 +1524,7 @@ try { } } - closeFieldModal() { - this.fieldModalOpen = false; - } + closeFieldModal() { this.fieldModalOpen = false; } handleFieldKey(key: string) { if (key === 'BACK') { @@ -1678,62 +1553,37 @@ try { this.secondGroup = ['F']; this.selectedNumbers = [...this.firstGroup, '-', 'F']; } - this.selectionService.updatePartial({ - label: this.selectedLabel, - numbers: [...this.selectedNumbers], - isBoxed: false - }); + 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 - }); + 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.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 - }); + 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.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 = []; - } + if (groupA.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupA; + else if (groupB.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupB; + else this.poolReplaceOptions = []; this.poolReplaceOpen = true; } @@ -1762,9 +1612,7 @@ try { this.selectionService.updatePartial({ label }); } - closePoolReplaceModal() { - this.poolReplaceOpen = false; - } + closePoolReplaceModal() { this.poolReplaceOpen = false; } treButtonClick(btnNum: number) { this.trePopupVisible = false; @@ -1785,80 +1633,59 @@ try { 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' }; + // Map button to pool + const poolInfo = btnNum === 2 ? { name: 'trb2' } : { 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.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; - } + closeTrePopup() { this.trePopupVisible = false; } + closeLimitPopup() { this.showLimitPopup = false; } copyNumbers() { - if (!this.selectedLabel) { - console.warn('Please select a label before copying numbers.'); - return; + 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(); } - 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; - } + trackByHorse(index: number, item: number): number { return item; } // Example usage of _.uniq for enabledHorseNumbers (if you ever set it) setEnabledHorseNumbers(numbers: number[]) { @@ -1869,43 +1696,22 @@ try { 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 + // 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; - 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 && + // 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; - console.log('[DEBUG] canEnterRow (non-WSP):', { - label: currentRow.label, - numbers: currentRow.numbers, - value: currentRow.value, - total: currentRow.total, - result - }); - return result; } -} \ No newline at end of file +} diff --git a/btc-UI/src/app/home/home.component.ts b/btc-UI/src/app/home/home.component.ts index c760191..3b874ba 100755 --- a/btc-UI/src/app/home/home.component.ts +++ b/btc-UI/src/app/home/home.component.ts @@ -49,6 +49,8 @@ export class HomeComponent implements OnInit, OnDestroy { console.log('🏠 HomeComponent loaded'); + + this.btcService.getAllRaceEventsToday().subscribe({ next: (response: HttpResponse) => { const horseRaceData = response.body; @@ -64,13 +66,13 @@ export class HomeComponent implements OnInit, OnDestroy { }, }); - const raceCardCached = localStorage.getItem('raceCardData'); + const raceCardCached = localStorage.getItem('rpinfo'); if (!raceCardCached) { this.btcService.getRaceCard().subscribe({ next: (res) => { const raceCardData = res.body; console.log('πŸ“¦ Race card preloaded:', raceCardData); - localStorage.setItem('raceCardData', JSON.stringify(raceCardData)); + localStorage.setItem('rpinfo', JSON.stringify(raceCardData)); this.updateRunnerCount(0); }, error: (err) => { @@ -118,7 +120,7 @@ export class HomeComponent implements OnInit, OnDestroy { } private updateRunnerCount(raceIdx: number) { - const raceCardData = JSON.parse(localStorage.getItem('raceCardData') || '{}'); + const raceCardData = JSON.parse(localStorage.getItem('rpinfo') || '{}'); const race = raceCardData?.raceVenueRaces?.races?.[raceIdx] || []; const runnerCount = Array.isArray(race) ? race.length : 12; if (!raceCardData?.raceVenueRaces?.races?.[raceIdx]) { diff --git a/btc-UI/src/app/login/login.component.ts b/btc-UI/src/app/login/login.component.ts index 1d45343..8355fb8 100755 --- a/btc-UI/src/app/login/login.component.ts +++ b/btc-UI/src/app/login/login.component.ts @@ -213,6 +213,7 @@ export class LoginComponent implements OnInit, OnDestroy { this.passwordStatus = true; } //--------------------------------------NEW LOGIN LOGIC ----------------------------------------// +//--------------------------------------NEW LOGIN LOGIC ----------------------------------------// async onSubmit(): Promise { if (this.loginForm.invalid) { this.loginForm.markAllAsTouched(); @@ -239,103 +240,37 @@ async onSubmit(): Promise { const btid = this.btcService.btid; console.log("πŸ“¦ BTID from file (via service):", btid); - // βœ… Prepare print data - const printData = { - name: userName, - employeeId: employeeId, - // btid: btid || "unknown", - action: 'login', - type: 'login' - }; - - console.log(printData.name); - console.log(printData.employeeId); - //console.log(printData.btid); - console.log(btid); - // βœ… Store in localStorage localStorage.setItem('userName', userName); localStorage.setItem('employeeId', employeeId); localStorage.setItem('password', password); localStorage.setItem('btid', btid || "unknown"); - // βœ… Print first β€” login only if printing succeeds - // fetch('http://localhost:9100/print', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(printData), - // }) - // .then((res) => { - // if (!res.ok) throw new Error('Print failed'); - // console.log('πŸ–¨οΈ Print successful'); + // βœ… Fetch race card after login success + this.btcService + .fetchRaceCard("021804111066", password, btid || "0483") // pass correct payload here + .subscribe({ + next: (rpinfo) => { + console.log("πŸ“¦ Race Card:", rpinfo); - // βœ… Only here we allow login - (window as any).electronAPI?.openSecondScreen?.(); - this.router.navigate(['/home']); - // }) - // .catch((err) => { - // console.error('‼️ Print failed', err); - // this.loginError = 'Login failed: printing service unavailable.'; - // this.passwordStatus = false; // reset status - // }); + // Save in localStorage for later use + localStorage.setItem('rpinfo', JSON.stringify(rpinfo)); + + // βœ… Navigate once race card stored + (window as any).electronAPI?.openSecondScreen?.(); + this.router.navigate(['/home']); + }, + error: (err) => { + console.error("‼️ Failed to fetch race card", err); + this.loginError = "Could not load race card."; + } + }); }, error: () => { this.loginError = 'Invalid login credentials'; } }); } -//-------------------------NEW LOGIN ENDS HERE -------------------------------------------------// - - -// async onSubmit(): Promise { -// if (this.loginForm.invalid) { -// this.loginForm.markAllAsTouched(); -// return; -// } - -// const { email, password } = this.loginForm.value; - -// // βœ… Await service (since it’s async) -// (await this.btcService.userLogin(email, password)).subscribe({ -// next: (response) => { -// const employee = response.body; -// console.log('🧠 Raw employee response:', employee); - -// const userName = employee?.userName || 'Unknown User'; -// const employeeId = employee?.userIdNumber || email; - -// console.log('βœ… Parsed name:', userName); -// console.log('βœ… Parsed ID:', employeeId); - -// this.passwordStatus = true; - -// // βœ… Get the BTID the service fetched -// const btid = this.btcService.btid; -// console.log("πŸ“¦ BTID from file (via service):", btid); - -// // Prepare print data -// const printData = { -// name: userName, -// employeeId: employeeId, -// action: 'login', -// type: 'login' -// }; - -// // βœ… Store in localStorage -// localStorage.setItem('userName', userName); -// localStorage.setItem('employeeId', employeeId); -// localStorage.setItem('password', password); -// localStorage.setItem('btid', btid || "unknown"); - -// // βœ… Open second screen + navigate -// (window as any).electronAPI?.openSecondScreen?.(); -// this.router.navigate(['/home']); -// }, -// error: () => { -// this.loginError = 'Invalid login credentials'; -// } -// }); -// } showConfirmModal : boolean = false; diff --git a/btc-UI/src/app/service/btc.service.ts b/btc-UI/src/app/service/btc.service.ts index c4b0398..ff98076 100755 --- a/btc-UI/src/app/service/btc.service.ts +++ b/btc-UI/src/app/service/btc.service.ts @@ -1,70 +1,154 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { ApplicationHttpRouts } from '../constants/http-routs'; +// import { HttpClient, HttpHeaders } from '@angular/common/http'; +// import { Injectable } from '@angular/core'; +// import { ApplicationHttpRouts } from '../constants/http-routs'; -@Injectable({ - providedIn: 'root', -}) +// @Injectable({ +// providedIn: 'root', +// }) + +// // export class BtcService { +// // constructor(private http: HttpClient) {} +// // btid: string | null = null; +// // //user login +// // public userLogin(employeeId: string, password: string) { +// // console.log('Login route ' + ApplicationHttpRouts.LOG_IN); +// // this.btid = localStorage.getItem('btid'); +// // if (this.btid) { +// // employeeId += "," + this.btid; +// // } else { +// // employeeId += ",null"; // or just "," if you don’t want "null" +// // } +// // return this.http.get(ApplicationHttpRouts.LOG_IN, { +// // headers: this.basicAuthCredentialsBuilder(employeeId, password), +// // withCredentials: true, +// // observe: 'response', +// // }); +// // } // export class BtcService { // constructor(private http: HttpClient) {} // btid: string | null = null; -// //user login -// public userLogin(employeeId: string, password: string) { -// console.log('Login route ' + ApplicationHttpRouts.LOG_IN); -// this.btid = localStorage.getItem('btid'); -// if (this.btid) { -// employeeId += "," + this.btid; -// } else { -// employeeId += ",null"; // or just "," if you don’t want "null" + +// // user login +// public async userLogin(employeeId: string, password: string) { +// console.log('Login route ' + ApplicationHttpRouts.LOG_IN); + +// try { +// if ((window as any).electronAPI?.getBtid) { +// this.btid = await (window as any).electronAPI.getBtid(); +// } else { +// console.warn('Electron API not available β€” fallback to null'); +// this.btid = null; +// } +// } catch (err) { +// console.error('Error fetching BTID:', err); +// this.btid = null; +// } + +// // Append BTID after comma +// if (this.btid) { +// employeeId += ',' + this.btid; +// } else { +// employeeId += ',null'; +// } + +// return this.http.get(ApplicationHttpRouts.LOG_IN, { +// headers: this.basicAuthCredentialsBuilder(employeeId, password), +// withCredentials: true, +// observe: 'response', +// }); // } -// return this.http.get(ApplicationHttpRouts.LOG_IN, { -// headers: this.basicAuthCredentialsBuilder(employeeId, password), + +// // what goes to the backend for auth ... is the same as postman's basic auth +// private basicAuthCredentialsBuilder( +// employeeOrUserId: string, +// password: string +// ): HttpHeaders { +// console.log(`username and password${employeeOrUserId} p = ${password}`); + +// return new HttpHeaders().set( +// 'Authorization', +// 'basic ' + window.btoa(employeeOrUserId + ':' + password) +// ); +// } + +// public pingLiveStatus() { +// return this.http.get(ApplicationHttpRouts.PING, { // withCredentials: true, // observe: 'response', +// responseType: 'text' as 'json', // }); // } +// // Fetch all race events today + +// public getAllRaceEventsToday() { +// return this.http.get(ApplicationHttpRouts.RACE_EVENTS_TODAY, { +// withCredentials: true, +// observe: 'response', +// responseType: 'json', +// }); +// } + +// public getRaceCard(){ +// return this.http.get(ApplicationHttpRouts.RACE_CARD, { +// withCredentials: true, +// observe: 'response', +// responseType: 'json', +// }) +// } +// } + + +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ApplicationHttpRouts } from '../constants/http-routs'; +import { of,Observable, tap } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) export class BtcService { constructor(private http: HttpClient) {} btid: string | null = null; + private raceCard: any = null; // cache race card in memory // user login public async userLogin(employeeId: string, password: string) { - console.log('Login route ' + ApplicationHttpRouts.LOG_IN); + console.log('Login route ' + ApplicationHttpRouts.LOG_IN); - try { - if ((window as any).electronAPI?.getBtid) { - this.btid = await (window as any).electronAPI.getBtid(); - } else { - console.warn('Electron API not available β€” fallback to null'); + try { + if ((window as any).electronAPI?.getBtid) { + this.btid = await (window as any).electronAPI.getBtid(); + } else { + console.warn('Electron API not available β€” fallback to null'); + this.btid = null; + } + } catch (err) { + console.error('Error fetching BTID:', err); this.btid = null; } - } catch (err) { - console.error('Error fetching BTID:', err); - this.btid = null; + + // Append BTID after comma + if (this.btid) { + employeeId += ',' + this.btid; + } else { + employeeId += ',null'; + } + + return this.http.get(ApplicationHttpRouts.LOG_IN, { + headers: this.basicAuthCredentialsBuilder(employeeId, password), + withCredentials: true, + observe: 'response', + }); } - // Append BTID after comma - if (this.btid) { - employeeId += ',' + this.btid; - } else { - employeeId += ',null'; - } - - return this.http.get(ApplicationHttpRouts.LOG_IN, { - headers: this.basicAuthCredentialsBuilder(employeeId, password), - withCredentials: true, - observe: 'response', - }); -} - - // what goes to the backend for auth ... is the same as postman's basic auth + // basic auth header private basicAuthCredentialsBuilder( employeeOrUserId: string, password: string ): HttpHeaders { - console.log(`username and password${employeeOrUserId} p = ${password}`); + console.log(`username and password = ${employeeOrUserId} p = ${password}`); return new HttpHeaders().set( 'Authorization', @@ -72,6 +156,7 @@ export class BtcService { ); } + // Ping backend public pingLiveStatus() { return this.http.get(ApplicationHttpRouts.PING, { withCredentials: true, @@ -81,7 +166,6 @@ export class BtcService { } // Fetch all race events today - public getAllRaceEventsToday() { return this.http.get(ApplicationHttpRouts.RACE_EVENTS_TODAY, { withCredentials: true, @@ -90,11 +174,75 @@ export class BtcService { }); } - public getRaceCard(){ - return this.http.get(ApplicationHttpRouts.RACE_CARD, { - withCredentials: true, - observe: 'response', - responseType: 'json', - }) + /** + * Fetch Race Card from backend (POST /download/rpinfo) + * Stores result in memory for reuse + // */ + // public fetchRaceCard( + // opCard: string, + // password: string, + // btId: string, + // usrId: string = '', + // btMake: number = 0 + // ): Observable { + // const payload = { opCard, password, btId, usrId, btMake }; + + // return this.http + // .post('http://localhost:8082/download/rpinfo', payload) + // .pipe( + // tap((data) => { + // console.log('πŸ“¦ Race Card fetched:', data); + // this.raceCard = data; // cache it + // }) + // ); + // } + + // /** + // * Return cached race card (if available) + // */ + // public getRaceCard(): any { + // return this.raceCard; + // } + + + public fetchRaceCard( + opCard: string, + password: string, + btId: string, + usrId: string = '', + btMake: number = 0 + ): Observable { + const payload = { opCard, password, btId, usrId, btMake }; + + return this.http + .post('http://localhost:8082/download/rpinfo', payload) + .pipe( + tap((data) => { + console.log('πŸ“¦ Race Card fetched:', data); + this.raceCard = data; // store in memory + localStorage.setItem('rpinfo', JSON.stringify(data)); // store in localStorage + }) + ); + } + + /** + * Return cached race card: + * - from memory (fastest) + * - fallback to localStorage (if reloaded) + */ + public getRaceCard(): Observable { + if (this.raceCard) { + return of(this.raceCard); + } + + const cached = localStorage.getItem('rpinfo'); + if (cached) { + this.raceCard = JSON.parse(cached); + return of(this.raceCard); + } + + // Nothing available, return empty object + return of(null); } } + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 243038e..f95d127 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -3,50 +3,23 @@ version: '3.8' # Define the services for our application stack services: - # PostgreSQL database service - postgres: - # Using the official PostgreSQL base image. - # 'postgres:16' is recommended for a stable, recent version. - # You can use 'postgres:latest' for the very newest, but versions like 13, 14, 15, 16 are common. - image: postgres:16 # Or postgres:latest, postgres:13-alpine, etc. - container_name: postgres_db # Assign a friendly name to the container - environment: - # --- CRITICAL: These settings will ONLY take effect if ./dbData directory is EMPTY on first run --- - POSTGRES_DB: horse # Your database name - POSTGRES_USER: postgres # Set to 'postgres', the default superuser for the official image - POSTGRES_PASSWORD: root # Your desired password for the 'postgres' user - # -------------------------------------------------------------------------------------------------- - ports: - - "5434:5432" # Map host port 5434 to container port 5432 - volumes: - # Using a bind mount. You MUST delete the ./dbData directory manually if you change user/pass/db. - # This directory MUST be empty when the container first starts to trigger initialization. - - ./dbData:/var/lib/postgresql/data # Persist PostgreSQL data to a local directory - # Added command for more verbose logging during startup (optional, but highly recommended here) - command: postgres -c log_statement=all - networks: - - app_network # Connect to our custom application network - # networks: - # - app_network # Connect to our custom application network + spring: #image: mathewfrancisv/spring_back_postgres:v1.0.0 - image: mathewfrancisv/btc_cezen_backend:v1.1.6 + image: mathewfrancisv/btc_cezen_backend:v2.3.0 container_name: spring_app ports: - "8083:8080" environment: # jdbc:postgresql://localhost:5432/horse # SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/horse - SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/horse - SPRING_DATASOURCE_USERNAME: postgres # Ensure this matches POSTGRES_USER - SPRING_DATASOURCE_PASSWORD: root # Ensure this matches POSTGRES_PASSWORD - SPRING_DATASOURCE_CORSIP: http://10.74.231.61:4200 + SPRING_DATASOURCE_CORSIP: http://10.236.119.124:4200 #network_mode: host networks: - app_network - depends_on: - - postgres + # depends_on: + # - postgres # Angular frontend application service angular-dev: