diff --git a/btc-UI/src/app/components/navbar/navbar.component.ts b/btc-UI/src/app/components/navbar/navbar.component.ts index 27e4cbc..3f16453 100755 --- a/btc-UI/src/app/components/navbar/navbar.component.ts +++ b/btc-UI/src/app/components/navbar/navbar.component.ts @@ -22,10 +22,9 @@ export class NavbarComponent implements OnInit, OnDestroy { btid: string | null = null; // component properties - add these at top-level in the component class -consecTimeouts = 0; -maxConsecTimeouts = 2; -liveStatusOk = true; - + consecTimeouts = 0; + maxConsecTimeouts = 2; + liveStatusOk = true; showVenueModal = false; showRaceModal = false; @@ -77,6 +76,12 @@ liveStatusOk = true; private http: HttpClient ) {} + // Stopbet-related properties + private stopbetStatuses: Map = new Map(); // raceNum => 'Y'|'N'|'S' + private currentVenue: string = ''; + private currentDate: string = ''; + private eventSource?: EventSource; + ngOnInit() { this.userName = localStorage.getItem('userName') || ''; this.btid = localStorage.getItem('btid'); @@ -117,14 +122,24 @@ liveStatusOk = true; } } + // Set current venue and date + this.currentVenue = (this.raceCardData?.venue ?? '').toUpperCase(); + this.currentDate = this.getTodayDate(); + + // Setup stopbet if venue is available + if (this.currentVenue) { + this.fetchInitialStopbets(); + this.setupSSE(); + } + // Periodic ABS/latest polling this.subscription = interval(5000) .pipe( switchMap(() => { - // console.log(`[ANGULAR] Fetching latest ABS status at ${new Date().toISOString()}`); + // console.log(`[ANGULAR] Fetching latest ABS status at ${new Date().toISOString()}`); return this.http.get('http://localhost:8080/abs/latest').pipe( catchError((err) => { - // console.error('[ANGULAR] ABS latest fetch failed ❌', err); + // console.error('[ANGULAR] ABS latest fetch failed ❌', err); this.liveStatusOk = false; return of(null); }) @@ -133,7 +148,7 @@ liveStatusOk = true; ) .subscribe((res: any) => { if (res) { - // console.log('[ANGULAR] ABS latest response ✅', res); + // console.log('[ANGULAR] ABS latest response ✅', res); this.liveStatusOk = res.success === true; // If backend eventually returns structured data: @@ -185,6 +200,97 @@ liveStatusOk = true; }); } + 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 fetchInitialStopbets() { + this.http.get(`http://localhost:8080/stopbet/raw?venue=${this.currentVenue}&date=${this.currentDate}`).subscribe({ + next: (res: any) => { + if (res.ok && res.data) { + const key = `${this.currentVenue}:${this.currentDate}`; + const meeting = res.data[key] || {}; + for (const race in meeting) { + const raceNum = parseInt(race, 10); + this.stopbetStatuses.set(raceNum, meeting[race]); + } + // Check and switch if current race is stopped + this.checkAndSwitchIfStopped(); + } + }, + error: (err) => { + console.error('[STOPBET] Failed to fetch initial statuses:', err); + } + }); + } + + private setupSSE() { + if (this.eventSource) { + this.eventSource.close(); + } + this.eventSource = new EventSource('http://localhost:8080/stopbet/stream'); + this.eventSource.onmessage = (event) => { + this.zone.run(() => { + try { + const data = JSON.parse(event.data); + if (data.type === 'stopbet' && data.venue === this.currentVenue && data.date === this.currentDate) { + const raceNum = parseInt(data.race, 10); + this.stopbetStatuses.set(raceNum, data.status); + // If current selected race is affected and now stopped, switch + if (this.selectedRace === raceNum && !this.isOpen(raceNum)) { + this.checkAndSwitchIfStopped(); + } + } + } catch (err) { + console.error('[STOPBET] SSE parse error:', err); + } + }); + }; + this.eventSource.onerror = (err) => { + console.error('[STOPBET] SSE error:', err); + }; + } + + private isOpen(race: number): boolean { + const status = this.stopbetStatuses.get(race); + return status === 'N' || status === undefined; // Assume open if unknown + } + + private getMaxRaces(): number { + return this.raceCardData?.raceVenueRaces?.races?.length || 10; + } + + private getOpenRaceStartingFrom(start: number): number { + const max = this.getMaxRaces(); + // Try from start to max + for (let r = start; r <= max; r++) { + if (this.isOpen(r)) { + return r; + } + } + // If none after, try from 1 to start-1 + for (let r = 1; r < start; r++) { + if (this.isOpen(r)) { + return r; + } + } + // All stopped, return original start + return start; + } + + private checkAndSwitchIfStopped() { + if (!this.isOpen(this.selectedRace)) { + const nextOpen = this.getOpenRaceStartingFrom(this.selectedRace + 1); + if (nextOpen !== this.selectedRace) { + this.selectRace(nextOpen); + } + } + } + private getLegIndexForRace(poolName: string, race: number): number { const raceMap: { [key: string]: number[] } = { mjp1: [1, 2, 3, 4], @@ -234,7 +340,7 @@ liveStatusOk = true; value: this.enabledHorseNumbers, }); - // console.log('[Multi-leg Pool] Updated enabled horse numbers:', this.enabledHorseNumbers); + // console.log('[Multi-leg Pool] Updated enabled horse numbers:', this.enabledHorseNumbers); } private getLegCountForLabel(): number { @@ -297,12 +403,12 @@ liveStatusOk = true; // Use lowercase 'venue' as structuredRaceCard uses .venue this.selectedVenue = this.raceCardData?.venue ?? this.raceCardData?.Venue ?? 'Select Venue'; this.updateEnabledHorseNumbers(); - // console.log('[MODAL] Opening venue modal (structured):', this.selectedVenue); + // console.log('[MODAL] Opening venue modal (structured):', this.selectedVenue); this.showVenueModal = true; } openRaceModal() { - // console.log('[MODAL] Opening race modal'); + // console.log('[MODAL] Opening race modal'); this.showRaceModal = true; const racesArr = this.raceCardData?.raceVenueRaces?.races ?? []; @@ -315,7 +421,7 @@ liveStatusOk = true; } closeModals() { - // console.log('[MODAL] Closing all modals'); + // console.log('[MODAL] Closing all modals'); this.showVenueModal = false; this.showRaceModal = false; this.showWalletModal = false; @@ -335,12 +441,26 @@ liveStatusOk = true; value: this.selectedVenue, }); - // console.log('[VENUE] Venue resolved to (structured):', this.selectedVenue, '| index:', index); + // Update currentVenue if changed + const newVenue = venue.toUpperCase(); + if (newVenue !== this.currentVenue) { + this.currentVenue = newVenue; + this.stopbetStatuses.clear(); + if (this.currentVenue) { + this.fetchInitialStopbets(); + this.setupSSE(); + } + } + + // console.log('[VENUE] Venue resolved to (structured):', this.selectedVenue, '| index:', index); this.closeModals(); } selectRace(race: number) { - this.selectedRace = race; + // Adjust race if the attempted one is stopped + const adjustedRace = this.isOpen(race) ? race : this.getOpenRaceStartingFrom(race); + + this.selectedRace = adjustedRace; this.currentPool = null; this.multiLegBaseRaceIdx = 0; this.currentLegRaceDisplay = ''; @@ -352,7 +472,7 @@ liveStatusOk = true; // Use this.raceCardData (structured) rather than re-parsing localStorage const raceList = this.raceCardData?.raceVenueRaces?.races ?? []; - const selectedRaceEntry = raceList[race - 1] ?? []; + const selectedRaceEntry = raceList[this.selectedRace - 1] ?? []; // Determine runnerCount defensively based on possible shapes let runnerCount = 12; @@ -367,7 +487,7 @@ liveStatusOk = true; this.sharedStateService.setRunnerCount(runnerCount); this.updateEnabledHorseNumbers(); - // console.log('[RACE] Race selected (structured):', this.selectedRace, '| Runner count:', runnerCount); + // console.log('[RACE] Race selected (structured):', this.selectedRace, '| Runner count:', runnerCount); this.closeModals(); } @@ -399,30 +519,30 @@ liveStatusOk = true; value: this.enabledHorseNumbers, }); - // console.log('[HORSE NUMBERS] Enabled horse numbers (structured):', this.enabledHorseNumbers); + // console.log('[HORSE NUMBERS] Enabled horse numbers (structured):', this.enabledHorseNumbers); } selectHorseNumber(number: number) { - // console.log('[HORSE] Selected horse number:', number); + // console.log('[HORSE] Selected horse number:', number); } openWalletModal() { - // console.log('[MODAL] Opening wallet modal'); + // console.log('[MODAL] Opening wallet modal'); this.showWalletModal = true; } openResultModal() { - // console.log('[MODAL] Opening result modal'); + // console.log('[MODAL] Opening result modal'); this.showResultModal = true; } openMessagesModal() { - // console.log('[MODAL] Opening messages modal'); + // console.log('[MODAL] Opening messages modal'); this.showMessagesModal = true; } openLogModal() { - // console.log('[MODAL] Opening log modal'); + // console.log('[MODAL] Opening log modal'); this.showLogModal = true; } @@ -492,8 +612,8 @@ liveStatusOk = true; displayBarcode = '********' + last4; } - // console.log(maskedBarcode); - // console.log('Decoded:', atob(maskedBarcode)); + // console.log(maskedBarcode); + // console.log('Decoded:', atob(maskedBarcode)); return { pool, @@ -509,12 +629,12 @@ liveStatusOk = true; }; }); } else { - // console.log('No tickets found in localStorage.'); + // console.log('No tickets found in localStorage.'); this.formattedTicketLogs = []; } this.showLogModal = true; - // console.log('Log modal opened. Final formattedTicketLogs:', this.formattedTicketLogs); + // console.log('Log modal opened. Final formattedTicketLogs:', this.formattedTicketLogs); } logout(): void { @@ -528,7 +648,7 @@ liveStatusOk = true; type: 'logout', // This is the missing piece }; - // console.log('[LOGOUT] Initiating logout with printData:', printData); + // console.log('[LOGOUT] Initiating logout with printData:', printData); fetch('http://localhost:9100/print', { method: 'POST', @@ -537,13 +657,13 @@ liveStatusOk = true; }) .then((res) => { if (!res.ok) throw new Error('Logout print failed'); - // console.log('[LOGOUT] Print successful'); + // console.log('[LOGOUT] Print successful'); (window as any).electronAPI?.closeSecondScreen?.(); localStorage.clear(); this.router.navigate(['/logout']); }) .catch((err) => { - // console.error('[LOGOUT] Error printing:', err); + // console.error('[LOGOUT] Error printing:', err); (window as any).electronAPI?.closeSecondScreen?.(); localStorage.clear(); this.router.navigate(['/logout']); @@ -554,11 +674,13 @@ liveStatusOk = true; if (this.subscription) { this.subscription.unsubscribe(); } + if (this.eventSource) { + this.eventSource.close(); + } } // Add trackByHorse for use in *ngFor trackByHorse(index: number, item: number): number { return item; } -} - +} \ No newline at end of file