import { Component, OnInit, HostListener, OnDestroy, NgZone, Inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { BtcService } from '../../service/btc.service'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { Router } from '@angular/router'; import { catchError, interval, of, Subscription, switchMap } from 'rxjs'; import { SharedStateService } from '../../service/shared-state.service'; import { StopbetService } from '../../service/stopbet.service'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', styleUrls: ['./navbar.component.css'], standalone: true, imports: [CommonModule, HttpClientModule], }) export class NavbarComponent implements OnInit, OnDestroy { dateTime: string = ''; isMenuOpen: boolean = false; screenWidth: number = window.innerWidth; private subscription!: Subscription; private stopbetSubscription: Subscription | null = null; userName: string = ''; btid: string | null = null; // Component properties consecTimeouts = 0; maxConsecTimeouts = 2; liveStatusOk = true; showVenueModal = false; showRaceModal = false; selectedVenue = 'Select Venue'; selectedRace: number = 1; currentLegRaceDisplay: string = ''; showWalletModal = false; showResultModal = false; showMessagesModal = false; showLogModal = false; raceCardData: any = {}; raceData: any[] = []; objectKeys = Object.keys; selectedRaceId: number = 0; enabledHorseNumbers: number[] = []; multiLegBaseRaceIdx = 0; currentPool: string | null = null; private prevEnabledKey = ''; wallet = { withdraw: 0, deposit: 0, payout: 0, cancel: 0, ticketing: 0, balance: 0, }; logs = [ { description: '', venue: '', ticketNumber: '', poolName: '', totalAmount: '', }, ]; messages: string[] = ['System ready.', 'Please select a venue.', 'Races updated.', 'Live status stable.']; // Stopbet-related properties private stopbetStatuses: Map = new Map(); private currentVenue: string = ''; private currentDate: string = ''; private eventSource: EventSource | undefined; stopMessage: string = ''; private stopMessageTimeout: number | null = null; formattedTicketLogs: { pool: string; horses: string; horsesArray: string[][]; ticketCountLabel: string; price: string; numbers: number[]; count: number; amount: number; maskedBarcode: string; displayBarcode: string; }[] = []; constructor( private btcService: BtcService, private router: Router, private sharedStateService: SharedStateService, private zone: NgZone, private http: HttpClient, @Inject(StopbetService) private stopbetService: StopbetService ) {} ngOnInit() { this.userName = localStorage.getItem('userName') || ''; 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); }); // Load structuredRaceCard from rpinfo if available (prefer structured) const rpCached = localStorage.getItem('rpinfo'); if (rpCached) { try { const parsed = JSON.parse(rpCached); this.raceCardData = parsed?.structuredRaceCard ?? {}; console.log('[INIT] Loaded structuredRaceCard from rpinfo:', this.raceCardData); } catch (err) { console.error('[INIT] Failed to parse rpinfo:', err); this.raceCardData = {}; } } else { // Fallback to older raceCardData key if present (non-structured) const fallback = localStorage.getItem('raceCardData'); if (fallback) { try { const parsed = JSON.parse(fallback); // prefer structuredRaceCard inside fallback if present this.raceCardData = parsed?.structuredRaceCard ?? parsed ?? {}; console.warn('[INIT] Loaded fallback raceCardData (prefer rpinfo/structuredRaceCard):', this.raceCardData); } catch (err) { console.error('[INIT] Failed to parse fallback raceCardData:', err); this.raceCardData = {}; } } else { this.raceCardData = {}; } } this.currentVenue = (this.raceCardData?.venue ?? '').toUpperCase() || localStorage.getItem('selectedVenue') || ''; this.currentDate = this.getTodayDate(); console.log('[INIT] Current venue:', this.currentVenue, 'date:', this.currentDate); this.selectedVenue = localStorage.getItem('selectedVenue') || this.raceCardData?.venue || 'Select Venue'; if (this.currentVenue) { try { this.stopbetService.initialize(this.currentVenue, this.currentDate); } catch (e) { console.warn('[STOPBET] stopbetService.initialize failed:', e); } } else { console.warn('[INIT] No venue set, skipping stopbetService.initialize'); } try { const statuses$ = this.stopbetService.getStopbetStatuses(); if (statuses$ && typeof statuses$.subscribe === 'function') { this.stopbetSubscription = statuses$.subscribe((statuses: Map) => { const newStatuses = new Map(statuses); const newStops: number[] = []; // Detect newly stopped races (same logic as before) newStatuses.forEach((value, key) => { const prev = this.stopbetStatuses.get(key); if (prev === 'N' || prev === undefined) { if (value === 'Y' || value === 'S') { newStops.push(key); } } }); this.stopbetStatuses = newStatuses; // If there are new stops, display only the latest one (most recent) if (newStops.length > 0) { newStops.sort((a, b) => a - b); // sort ascending const latestStop = newStops[newStops.length - 1]; // pick the highest (latest) race number detected this.stopMessage = `Race ${latestStop} Stopped`; // Sync new single stop message to electron main if available if ((window as any).electronAPI) { try { (window as any).electronAPI.syncStopMessage(this.stopMessage); } catch (e) { console.warn('[NAVBAR] electronAPI.syncStopMessage failed:', e); } } // Clear any existing timeout so message stays visible full duration if (this.stopMessageTimeout) { clearTimeout(this.stopMessageTimeout); } // Keep message visible for 50 seconds (50000 ms) then clear this.stopMessageTimeout = window.setTimeout(() => { this.stopMessage = ''; this.stopMessageTimeout = null; // Sync cleared message to electron main if available if ((window as any).electronAPI) { try { (window as any).electronAPI.syncStopMessage(this.stopMessage); } catch (e) { console.warn('[NAVBAR] electronAPI.syncStopMessage (clear) failed:', e); } } }, 50000); } console.log('[NAVBAR] Updated stopbetStatuses:', Object.fromEntries(this.stopbetStatuses)); this.checkAndSwitchIfStopped(); this.updateEnabledHorseNumbers(); }); } } catch (e) { console.warn('[STOPBET] Subscribe to service failed:', e); } this.subscription = interval(5000) .pipe( switchMap(() => { return this.http.get('http://localhost:8087/abs/latest').pipe( catchError((err) => { this.liveStatusOk = false; return of(null); }) ); }) ) .subscribe((res: any) => { if (res) { this.liveStatusOk = res.success === true; if (res.raceCardData) { this.raceCardData = res.raceCardData; localStorage.setItem('raceCardData', JSON.stringify(res.raceCardData)); const newVenue = (this.raceCardData?.venue ?? '').toUpperCase(); if (newVenue && newVenue !== this.currentVenue) { console.log('[NAVBAR] Venue changed to', newVenue, 'reinitializing stopbetService'); this.currentVenue = newVenue; this.stopbetService.initialize(this.currentVenue, this.currentDate); } } if (res.wallet) { this.wallet = res.wallet; } } this.updateEnabledHorseNumbers(); }); // Subscribe to shared state updates this.sharedStateService.sharedData$.subscribe((data) => { if (data.type === 'currentLegRace') { this.selectedRace = data.value; const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedRace(this.selectedRace); } if (this.currentPool) { const leg = this.getLegIndexForRace(this.currentPool, data.value); this.currentLegRaceDisplay = `Leg ${leg + 1} (Race ${data.value}) for ${this.currentPool}`; this.updateEnabledHorseNumbers(); } else { this.currentLegRaceDisplay = ''; this.updateEnabledHorseNumbers(); } } if (data.type === 'multiLegPoolStart') { const { label, baseRaceIdx } = data.value; this.currentPool = label; this.multiLegBaseRaceIdx = baseRaceIdx; this.selectedRace = this.getValidRaceForLeg(label, 0); const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedRace(this.selectedRace); } this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${label}`; this.updateEnabledHorseNumbers(); } if (data.type === 'multiLegPoolEnd') { this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; // Reset selectedRace to the first open race (1 if no stops, or the running/open one if stops exist) this.selectedRace = this.getOpenRaceStartingFrom(1); this.sharedStateService.updateSharedData({ type: 'selectedRace', value: this.selectedRace, }); const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedRace(this.selectedRace); } this.updateEnabledHorseNumbers(); } if (data.type === 'selectedRace') { this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.selectedRace = data.value; const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedRace(this.selectedRace); } this.updateEnabledHorseNumbers(); } if (data.type === 'selectedVenue') { const newVenue = data.value.toUpperCase(); if (newVenue !== this.currentVenue) { console.log('[NAVBAR] Selected venue changed to', newVenue); this.currentVenue = newVenue; localStorage.setItem('selectedVenue', newVenue); this.stopbetService.initialize(this.currentVenue, this.currentDate); } } }); // Initial sync for venue and race const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedVenue(this.selectedVenue); electronAPI.syncSelectedRace(this.selectedRace); } } private getTodayDate(): string { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}/${month}/${day}`; } private isOpen(race: number): boolean { return this.stopbetService.isOpen(race); } private getMaxRaces(): number { return this.raceCardData?.raceVenueRaces?.races?.length || 10; } private getOpenRaceStartingFrom(start: number) { const max = this.getMaxRaces(); return this.stopbetService.getOpenRaceStartingFrom(start, max); } private checkAndSwitchIfStopped() { if (!this.isOpen(this.selectedRace)) { const nextOpen = this.getOpenRaceStartingFrom(this.selectedRace + 1); if (nextOpen !== this.selectedRace) { console.log('[NAVBAR] Switching from stopped race', this.selectedRace, 'to', nextOpen); this.selectRace(nextOpen); } } } private getLegIndexForRace(poolName: string | null, race: number): number { if (!poolName) return 0; 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.toLowerCase()]?.indexOf(race) ?? 0; } private getRaceForLeg(poolName: string | null, leg: number): number { if (!poolName) return this.multiLegBaseRaceIdx + leg; 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.toLowerCase()]?.[leg] ?? this.multiLegBaseRaceIdx + leg; } private getValidRaceForLeg(poolName: string, leg: number): number { const races = this.getValidRacesForPool(poolName); return races[leg] || this.multiLegBaseRaceIdx + leg; } private getValidRacesForPool(poolName: string): 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], }; const defaultRaces = raceMap[poolName.toLowerCase()] || []; const maxRaces = this.getMaxRaces(); let earliestStoppedRace: number | null = null; this.stopbetStatuses.forEach((status, raceNum) => { if (status !== 'N' && status !== undefined && (earliestStoppedRace === null || raceNum < earliestStoppedRace)) { earliestStoppedRace = raceNum; } }); const validRaces = defaultRaces.filter((race) => earliestStoppedRace === null ? this.isOpen(race) && race <= maxRaces : race > earliestStoppedRace && this.isOpen(race) && race <= maxRaces ); console.log('[NAVBAR] Valid races for pool', poolName, ':', validRaces); return validRaces; } private getLegCountForLabel(): number { if (!this.currentPool) return 1; switch (this.currentPool.toLowerCase()) { case 'mjp1': return 4; case 'jkp1': return 5; case 'trb1': case 'trb2': return 3; default: return 1; } } updateEnabledHorseNumbers() { const key = `${this.currentPool || 'single'}-${this.selectedRace}-${this.multiLegBaseRaceIdx}`; if (this.prevEnabledKey === key) return; this.prevEnabledKey = key; if (!this.isOpen(this.selectedRace)) { this.enabledHorseNumbers = []; this.sharedStateService.updateSharedData({ type: 'enabledHorseNumbers', value: this.enabledHorseNumbers, }); console.log('[NAVBAR] Race', this.selectedRace, 'is stopped, enabledHorseNumbers:', this.enabledHorseNumbers); return; } const racesArr = this.raceCardData?.raceVenueRaces?.races ?? []; let runners: any[] = []; if (this.currentPool) { const legCount = this.getLegCountForLabel(); const validRaces = this.getValidRacesForPool(this.currentPool); let combinedHorseNumbers: number[] = []; for (let leg = 0; leg < legCount; leg++) { const raceNum = validRaces[leg]; if (!raceNum || !this.isOpen(raceNum)) continue; const raceIdx = raceNum - 1; const rawRace = racesArr[raceIdx] ?? []; if (Array.isArray(rawRace)) { runners = rawRace; } else if (rawRace && Array.isArray(rawRace.runners)) { runners = rawRace.runners; } else { runners = []; } const horses = runners .map((runner: any) => runner?.horseNumber ?? runner?.number ?? runner?.horse_no) .filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30); combinedHorseNumbers.push(...horses); } this.enabledHorseNumbers = Array.from(new Set(combinedHorseNumbers)); } else { const raceIndex = this.selectedRace - 1; const rawRace = racesArr[raceIndex] ?? []; if (Array.isArray(rawRace)) { runners = rawRace; } else if (rawRace && Array.isArray(rawRace.runners)) { runners = rawRace.runners; } else { runners = []; } this.enabledHorseNumbers = runners .map((r: any) => r?.horseNumber ?? r?.number ?? r?.horse_no) .filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30); } this.sharedStateService.updateSharedData({ type: 'enabledHorseNumbers', value: this.enabledHorseNumbers, }); console.log('[NAVBAR] Updated enabledHorseNumbers:', this.enabledHorseNumbers); } updateDateTime() { const now = new Date(); this.dateTime = now.toLocaleString(); } @HostListener('window:resize', ['$event']) onResize(event: any) { this.screenWidth = event.target.innerWidth; if (this.screenWidth > 800) { this.isMenuOpen = false; } } toggleMenu() { this.isMenuOpen = !this.isMenuOpen; } openVenueModal() { if (!this.raceCardData || Object.keys(this.raceCardData).length === 0) { const cachedData = localStorage.getItem('raceCardData'); if (cachedData) { try { const parsed = JSON.parse(cachedData); this.raceCardData = parsed?.structuredRaceCard ?? parsed ?? { raceVenueRaces: { races: [] }, venue: 'Unknown Venue' }; console.log('[VENUE MODAL] Loaded cached raceCardData:', this.raceCardData); } catch (e) { console.error('[VENUE MODAL] Failed to parse cached raceCardData:', e); this.raceCardData = { raceVenueRaces: { races: [] }, venue: 'Unknown Venue' }; } } else { this.raceCardData = { raceVenueRaces: { races: [] }, venue: 'Unknown Venue' }; console.log('[VENUE MODAL] No cached data, using default raceCardData:', this.raceCardData); } } // Set selectedVenue based on raceCardData this.selectedVenue = this.raceCardData?.venue ?? this.raceCardData?.Venue ?? 'Select Venue'; console.log('[VENUE MODAL] Setting selectedVenue:', this.selectedVenue); this.updateEnabledHorseNumbers(); // Ensure this is called if needed this.showVenueModal = true; } openRaceModal() { this.showRaceModal = true; const racesArr = this.raceCardData?.raceVenueRaces?.races ?? []; if (typeof this.selectedRaceId === 'number' && racesArr.length > this.selectedRaceId) { const maybeRace = racesArr[this.selectedRaceId]; this.raceData = Array.isArray(maybeRace) ? maybeRace : maybeRace?.runners ?? []; } else { this.raceData = []; } } closeModals() { this.showVenueModal = false; this.showRaceModal = false; this.showWalletModal = false; this.showResultModal = false; this.showMessagesModal = false; this.showLogModal = false; } selectVenue(index: number) { const venue = this.raceCardData?.venue ?? this.raceCardData?.Venue ?? 'Unknown Venue'; this.selectedVenue = venue; this.selectedRaceId = index; this.sharedStateService.updateSharedData({ type: 'selectedVenue', value: this.selectedVenue, }); const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedVenue(this.selectedVenue); } const newVenue = venue.toUpperCase(); if (newVenue !== this.currentVenue) { this.currentVenue = newVenue; this.stopbetStatuses.clear(); if (this.currentVenue) { try { console.log('[NAVBAR] Initializing stopbetService for venue:', newVenue); this.stopbetService.initialize(this.currentVenue, this.currentDate); } catch {} } } this.closeModals(); } selectRace(race: number) { const adjustedRace = this.isOpen(race) ? race : this.getOpenRaceStartingFrom(race); this.selectedRace = adjustedRace; this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; this.sharedStateService.updateSharedData({ type: 'selectedRace', value: this.selectedRace, }); const electronAPI = (window as any).electronAPI; if (electronAPI) { electronAPI.syncSelectedRace(this.selectedRace); } const raceList = this.raceCardData?.raceVenueRaces?.races ?? []; const selectedRaceEntry = raceList[this.selectedRace - 1] ?? []; let runnerCount = 12; if (Array.isArray(selectedRaceEntry)) { runnerCount = selectedRaceEntry.length || 12; } else if (selectedRaceEntry && Array.isArray(selectedRaceEntry.runners)) { runnerCount = selectedRaceEntry.runners.length || 12; } else if (selectedRaceEntry && typeof selectedRaceEntry.runnerCount === 'number') { runnerCount = selectedRaceEntry.runnerCount; } this.sharedStateService.setRunnerCount(runnerCount); this.updateEnabledHorseNumbers(); this.closeModals(); } selectHorseNumber(number: number) { // Intentionally left no-op (UI hook) } openWalletModal() { this.showWalletModal = true; } openResultModal() { this.showResultModal = true; } openMessagesModal() { this.showMessagesModal = true; } openLogModal() { this.showLogModal = true; } openViewLog() { const storedTickets = localStorage.getItem('localTicketsViewlog'); if (storedTickets) { const tickets = JSON.parse(storedTickets); this.formattedTicketLogs = tickets.map((ticket: any, index: number) => { 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 = ''; if (rawLabel) { const parts = rawLabel.split(/\s+/); pool = parts[0]; const countMatch = rawLabel.match(/\*(\d+)(?:\s+(Rs\s*\d+))?/); if (countMatch) { ticketCountLabel = `*${countMatch[1]}`; price = countMatch[2] || ''; } const horsesPartMatch = rawLabel.match(/^\w+\s+(.+?)\s+\*\d+/); if (horsesPartMatch) { horses = horsesPartMatch[1].trim(); if (['MJP', 'JKP', 'TRE'].includes(pool)) { horsesArray = horses.split('/').map((r) => r.trim().split(',').map((h) => h.trim())); } else { horsesArray = [horses.split(',').map((h) => h.trim())]; } } } if (barcodeId) { const last4 = barcodeId.slice(-4); const encryptedPart = btoa(barcodeId.slice(0, -4)); maskedBarcode = encryptedPart; displayBarcode = '********' + last4; } return { pool, horses, horsesArray, ticketCountLabel, price, numbers, count, amount, maskedBarcode, displayBarcode, }; }); } else { this.formattedTicketLogs = []; } this.showLogModal = true; } logout(): void { const name = localStorage.getItem('userName') || 'Unknown User'; const employeeId = localStorage.getItem('employeeId') || '000000'; const stopbetData = localStorage.getItem('stopbetStatuses'); const printData = { name, employeeId, action: 'logout', type: 'logout', }; 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'); (window as any).electronAPI?.closeSecondScreen?.(); localStorage.clear(); if (stopbetData) { localStorage.setItem('stopbetStatuses', stopbetData); console.log('[NAVBAR] Preserved stopbetStatuses in localStorage:', stopbetData); } this.router.navigate(['/logout']); }) .catch((err) => { console.error('[NAVBAR] Logout error:', err); (window as any).electronAPI?.closeSecondScreen?.(); localStorage.clear(); if (stopbetData) { localStorage.setItem('stopbetStatuses', stopbetData); console.log('[NAVBAR] Preserved stopbetStatuses in localStorage:', stopbetData); } this.router.navigate(['/logout']); }); } ngOnDestroy(): void { if (this.subscription) { this.subscription.unsubscribe(); } if (this.stopbetSubscription) { this.stopbetSubscription.unsubscribe(); } if (this.eventSource) { try { this.eventSource.close(); } catch {} } if (this.stopMessageTimeout) { clearTimeout(this.stopMessageTimeout); } } trackByHorse(index: number, item: number): number { return item; } }