From 5984f9d036f75ab3d8ded9b061a41b410b544ce2 Mon Sep 17 00:00:00 2001 From: karthik Date: Mon, 15 Sep 2025 16:29:46 +0530 Subject: [PATCH] fix : added service for the stop bet --- .../app/components/navbar/navbar.component.ts | 202 ++++++++++-------- .../touch-pad-menu.component.ts | 115 +++++++--- btc-UI/src/app/service/stopbet.service.ts | 94 ++++++++ 3 files changed, 297 insertions(+), 114 deletions(-) create mode 100644 btc-UI/src/app/service/stopbet.service.ts diff --git a/btc-UI/src/app/components/navbar/navbar.component.ts b/btc-UI/src/app/components/navbar/navbar.component.ts index 3f16453..398f9a7 100755 --- a/btc-UI/src/app/components/navbar/navbar.component.ts +++ b/btc-UI/src/app/components/navbar/navbar.component.ts @@ -1,10 +1,12 @@ -import { Component, OnInit, HostListener, OnDestroy, NgZone } from '@angular/core'; +// navbar.component.ts +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', @@ -18,6 +20,7 @@ export class NavbarComponent implements OnInit, OnDestroy { isMenuOpen: boolean = false; screenWidth: number = window.innerWidth; private subscription!: Subscription; + private stopbetSubscription: Subscription | null = null; userName: string = ''; btid: string | null = null; @@ -68,20 +71,34 @@ export class NavbarComponent implements OnInit, OnDestroy { messages: string[] = ['System ready.', 'Please select a venue.', 'Races updated.', 'Live status stable.']; + // Stopbet-related properties (merged approach) + private stopbetStatuses: Map = new Map(); // raceNum => 'Y'|'N'|'S' + private currentVenue: string = ''; + private currentDate: string = ''; + private eventSource?: EventSource; + + 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 + private http: HttpClient, + @Inject(StopbetService) private stopbetService: StopbetService ) {} - // 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'); @@ -93,7 +110,7 @@ export class NavbarComponent implements OnInit, OnDestroy { }, 1000); }); - // === Load structuredRaceCard from localdb (rpinfo) if available === + // === Load structuredRaceCard from rpinfo if available (prefer structured) === const rpCached = localStorage.getItem('rpinfo'); if (rpCached) { try { @@ -126,20 +143,52 @@ export class NavbarComponent implements OnInit, OnDestroy { this.currentVenue = (this.raceCardData?.venue ?? '').toUpperCase(); this.currentDate = this.getTodayDate(); - // Setup stopbet if venue is available + // Setup stopbet if venue is available: + // 1) initialize StopbetService (if it uses BroadcastChannel or another approach) if (this.currentVenue) { + try { + this.stopbetService.initialize(this.currentVenue, this.currentDate); + } catch (e) { + // initialization may not be necessary or supported by the service; ignore if fails + console.warn('[STOPBET] stopbetService.initialize failed or not required:', e); + } + + // 2) fetch initial stopbets (HTTP fallback) and setup SSE stream this.fetchInitialStopbets(); this.setupSSE(); } - // Periodic ABS/latest polling + // 3) subscribe to StopbetService (if it provides an observable of statuses) + try { + const statuses$ = (this.stopbetService as any).getStopbetStatuses?.(); + if (statuses$ && typeof statuses$.subscribe === 'function') { + this.stopbetSubscription = statuses$.subscribe((statuses: any) => { + // if the service gives you a map/object, merge into local stopbetStatuses + if (statuses && typeof statuses === 'object') { + try { + // expected structure: { raceNum: status, ... } or Map-like + Object.entries(statuses).forEach(([k, v]) => { + const rn = Number(k); + if (!Number.isNaN(rn)) this.stopbetStatuses.set(rn, String(v)); + }); + } catch { + // ignore malformed + } + } + // Trigger check/switch if current race is stopped + this.checkAndSwitchIfStopped(); + }); + } + } catch (e) { + console.warn('[STOPBET] subscribe to service failed:', e); + } + + // Periodic ABS/latest polling (unchanged) this.subscription = interval(5000) .pipe( switchMap(() => { - // 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); this.liveStatusOk = false; return of(null); }) @@ -148,14 +197,11 @@ export class NavbarComponent implements OnInit, OnDestroy { ) .subscribe((res: any) => { if (res) { - // console.log('[ANGULAR] ABS latest response ✅', res); this.liveStatusOk = res.success === true; - // If backend eventually returns structured data: if (res.raceCardData) { this.raceCardData = res.raceCardData; localStorage.setItem('raceCardData', JSON.stringify(res.raceCardData)); - // keep rpinfo consistent if desired (do not overwrite rpinfo unless you want to) } if (res.wallet) { this.wallet = res.wallet; @@ -182,7 +228,6 @@ export class NavbarComponent implements OnInit, OnDestroy { this.multiLegBaseRaceIdx = baseRaceIdx; this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${label}`; this.updateEnabledHorseNumbersForMultiLeg(baseRaceIdx); - //console.log(`[Multi-leg Pool] Selected: ${label}, Base Race: ${baseRaceIdx}`); } if (data.type === 'multiLegPoolEnd') { this.currentPool = null; @@ -208,17 +253,22 @@ export class NavbarComponent implements OnInit, OnDestroy { return `${year}/${month}/${day}`; } + /** + * HTTP-based initial fetch (fallback). + * Keeps internal stopbetStatuses Map updated with server data if available. + */ private fetchInitialStopbets() { + if (!this.currentVenue || !this.currentDate) return; this.http.get(`http://localhost:8080/stopbet/raw?venue=${this.currentVenue}&date=${this.currentDate}`).subscribe({ next: (res: any) => { - if (res.ok && res.data) { + if (res && 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 + // After loading, ensure current selected race is valid this.checkAndSwitchIfStopped(); } }, @@ -228,57 +278,63 @@ export class NavbarComponent implements OnInit, OnDestroy { }); } + /** + * SSE stream setup for real-time stopbet updates. + */ 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(); + try { + 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); } - } catch (err) { - console.error('[STOPBET] SSE parse error:', err); - } - }); - }; - this.eventSource.onerror = (err) => { - console.error('[STOPBET] SSE error:', err); - }; + }); + }; + this.eventSource.onerror = (err) => { + console.error('[STOPBET] SSE error:', err); + // optionally attempt reconnect logic here + }; + } catch (err) { + console.warn('[STOPBET] SSE initialization failed:', err); + } } private isOpen(race: number): boolean { + // Prefer explicit map; if unknown assume open const status = this.stopbetStatuses.get(race); - return status === 'N' || status === undefined; // Assume open if unknown + return status === 'N' || status === undefined; } private getMaxRaces(): number { return this.raceCardData?.raceVenueRaces?.races?.length || 10; } - private getOpenRaceStartingFrom(start: number): number { + private getOpenRaceStartingFrom(start: 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; } @@ -339,8 +395,6 @@ export class NavbarComponent implements OnInit, OnDestroy { type: 'enabledHorseNumbers', value: this.enabledHorseNumbers, }); - - // console.log('[Multi-leg Pool] Updated enabled horse numbers:', this.enabledHorseNumbers); } private getLegCountForLabel(): number { @@ -403,12 +457,10 @@ export class NavbarComponent implements OnInit, OnDestroy { // 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); this.showVenueModal = true; } openRaceModal() { - // console.log('[MODAL] Opening race modal'); this.showRaceModal = true; const racesArr = this.raceCardData?.raceVenueRaces?.races ?? []; @@ -421,7 +473,6 @@ export class NavbarComponent implements OnInit, OnDestroy { } closeModals() { - // console.log('[MODAL] Closing all modals'); this.showVenueModal = false; this.showRaceModal = false; this.showWalletModal = false; @@ -431,7 +482,6 @@ export class NavbarComponent implements OnInit, OnDestroy { } selectVenue(index: number) { - // We expect this.raceCardData to be structuredRaceCard (venue field lowercase) const venue = this.raceCardData?.venue ?? this.raceCardData?.Venue ?? 'Unknown Venue'; this.selectedVenue = venue; this.selectedRaceId = index; @@ -441,7 +491,6 @@ export class NavbarComponent implements OnInit, OnDestroy { value: this.selectedVenue, }); - // Update currentVenue if changed const newVenue = venue.toUpperCase(); if (newVenue !== this.currentVenue) { this.currentVenue = newVenue; @@ -449,10 +498,12 @@ export class NavbarComponent implements OnInit, OnDestroy { if (this.currentVenue) { this.fetchInitialStopbets(); this.setupSSE(); + try { + this.stopbetService.initialize(this.currentVenue, this.currentDate); + } catch {} } } - // console.log('[VENUE] Venue resolved to (structured):', this.selectedVenue, '| index:', index); this.closeModals(); } @@ -486,8 +537,6 @@ export class NavbarComponent implements OnInit, OnDestroy { this.sharedStateService.setRunnerCount(runnerCount); this.updateEnabledHorseNumbers(); - - // console.log('[RACE] Race selected (structured):', this.selectedRace, '| Runner count:', runnerCount); this.closeModals(); } @@ -518,47 +567,28 @@ export class NavbarComponent implements OnInit, OnDestroy { type: 'enabledHorseNumbers', value: this.enabledHorseNumbers, }); - - // console.log('[HORSE NUMBERS] Enabled horse numbers (structured):', this.enabledHorseNumbers); } selectHorseNumber(number: number) { - // console.log('[HORSE] Selected horse number:', number); + // Intentionally left no-op (UI hook) } openWalletModal() { - // console.log('[MODAL] Opening wallet modal'); this.showWalletModal = true; } openResultModal() { - // console.log('[MODAL] Opening result modal'); this.showResultModal = true; } openMessagesModal() { - // console.log('[MODAL] Opening messages modal'); this.showMessagesModal = true; } openLogModal() { - // console.log('[MODAL] Opening log modal'); this.showLogModal = true; } - 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'); @@ -612,9 +642,6 @@ export class NavbarComponent implements OnInit, OnDestroy { displayBarcode = '********' + last4; } - // console.log(maskedBarcode); - // console.log('Decoded:', atob(maskedBarcode)); - return { pool, horses, @@ -629,12 +656,10 @@ export class NavbarComponent implements OnInit, OnDestroy { }; }); } else { - // console.log('No tickets found in localStorage.'); this.formattedTicketLogs = []; } this.showLogModal = true; - // console.log('Log modal opened. Final formattedTicketLogs:', this.formattedTicketLogs); } logout(): void { @@ -645,11 +670,9 @@ export class NavbarComponent implements OnInit, OnDestroy { name, employeeId, action: 'logout', - type: 'logout', // This is the missing piece + type: 'logout', }; - // console.log('[LOGOUT] Initiating logout with printData:', printData); - fetch('http://localhost:9100/print', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -657,13 +680,11 @@ export class NavbarComponent implements OnInit, OnDestroy { }) .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']); @@ -674,8 +695,13 @@ export class NavbarComponent implements OnInit, OnDestroy { if (this.subscription) { this.subscription.unsubscribe(); } + if (this.stopbetSubscription) { + this.stopbetSubscription.unsubscribe(); + } if (this.eventSource) { - this.eventSource.close(); + try { + this.eventSource.close(); + } catch {} } } @@ -683,4 +709,4 @@ export class NavbarComponent implements OnInit, OnDestroy { trackByHorse(index: number, item: number): number { return item; } -} \ No newline at end of file +} 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 7b821aa..5c2ecc9 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 @@ -1,3 +1,4 @@ +// touch-pad-menu.component.ts import { Component, Input, @@ -5,14 +6,15 @@ import { OnDestroy, NgZone, ChangeDetectionStrategy, - ChangeDetectorRef // <-- Add this import + ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Subscription } from 'rxjs'; import { SelectionService, SelectionData } from '../selection.service/selection.service'; import { SharedStateService } from '../../service/shared-state.service'; import { LabelRestrictionService } from '../selection.service/label-restriction.service'; -import _, { join } from 'lodash'; +import { StopbetService } from '../../service/stopbet.service'; // Import StopbetService +import _ from 'lodash'; @Component({ selector: 'app-touch-pad-menu', @@ -87,6 +89,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { private currentRowSubscription: Subscription | null = null; private selectionsSubscription: Subscription | null = null; private runnerCountSubscription: Subscription | null = null; + private stopbetSubscription: Subscription | null = null; // Subscription for stopbet statuses private currentTotal: number = 0; private currentSelections: SelectionData[] = []; @@ -98,21 +101,17 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { structuredRaceCard: any = {}; selectedRaceNumber: string = '1'; - selectionService: SelectionService; - sharedStateService: SharedStateService; - labelRestrictionService: LabelRestrictionService; + + private stopbetStatuses: Map = new Map(); // Local copy of stopbet statuses constructor( - selectionService: SelectionService, - sharedStateService: SharedStateService, - labelRestrictionService: LabelRestrictionService, + private selectionService: SelectionService, + private sharedStateService: SharedStateService, + private labelRestrictionService: LabelRestrictionService, + private stopbetService: StopbetService, // Inject StopbetService private ngZone: NgZone, - private cdr: ChangeDetectorRef // <-- Inject ChangeDetectorRef - ) { - this.selectionService = selectionService; - this.sharedStateService = sharedStateService; - this.labelRestrictionService = labelRestrictionService; - } + private cdr: ChangeDetectorRef + ) {} ngOnInit() { // Prefer rpinfo.structuredRaceCard @@ -126,6 +125,14 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } this.raceCardData = this.structuredRaceCard; + // Subscribe to stopbet statuses + this.stopbetSubscription = this.stopbetService.getStopbetStatuses().subscribe((statuses) => { + this.stopbetStatuses = new Map(statuses); + this.refreshBlockedLabels(this.selectedLabel); + this.setActualRunners(); + this.cdr.markForCheck(); + }); + this.runnerCountSubscription = this.sharedStateService.runnerCount$.subscribe((count: number) => { this.runnerCount = count || 12; this.numbers = Array.from({ length: 30 }, (_, i) => i + 1); @@ -134,6 +141,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.btid = localStorage.getItem('btid'); // --- NEW: Update actualRunners when runner count changes --- this.setActualRunners(); + this.cdr.markForCheck(); }); this.labelRowsFlat = this.labelRows.flat(); @@ -150,10 +158,12 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { if (!this.totalAmountLimitReached) { this.showLimitPopup = false; } + this.cdr.markForCheck(); }); this.currentRowSubscription = this.selectionService.currentRow$.subscribe((row: SelectionData) => { this.currentTotal = row.total || 0; + this.cdr.markForCheck(); }); // Subscribe to selectedRace (from navbar) @@ -170,6 +180,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } else { this.setActualRunners(); } + this.cdr.markForCheck(); }); // legacy storage - keep for compatibility if rpinfo absent @@ -183,6 +194,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.currentRowSubscription?.unsubscribe(); this.selectionsSubscription?.unsubscribe(); this.runnerCountSubscription?.unsubscribe(); + this.stopbetSubscription?.unsubscribe(); } private safeGetJSON(key: string): any | null { @@ -221,9 +233,12 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { 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))); + // Check if the race is open + if (!this.stopbetStatuses.has(raceIdx + 1) || this.stopbetStatuses.get(raceIdx + 1) === 'N') { + const race = races[raceIdx]; + if (race?.horses && Array.isArray(race.horses)) { + return new Set(race.horses.map((num: any) => Number(num))); + } } return new Set(); } @@ -241,9 +256,12 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { getActualRunnersForCurrentRace(): Set { 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))); + // Check if the race is open + if (!this.stopbetStatuses.has(selectedRaceIdx + 1) || this.stopbetStatuses.get(selectedRaceIdx + 1) === 'N') { + const race = races[selectedRaceIdx]; + if (race?.horses && Array.isArray(race.horses)) { + return new Set(race.horses.map((num: any) => Number(num))); + } } return new Set(); } @@ -324,9 +342,30 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } isLabelDisabled(label: string): boolean { - return this.disabledLabels.includes(label) || - this.totalAmountLimitReached || - this.blockedLabels.has(label); + if (this.disabledLabels.includes(label) || this.totalAmountLimitReached || this.blockedLabels.has(label)) return true; + + // Additional check for multi-leg pools: disable if any race in the pool is stopped + if (this.multiLegLabels.includes(label)) { + const poolName = label === 'MJP' ? 'mjp1' : label === 'JKP' ? 'jkp1' : 'trb1'; + const raceIndices = this.getRaceIndicesForPool(poolName); + if (raceIndices.some(race => !this.isOpen(race))) { + return true; + } + } + return false; + } + + private getRaceIndicesForPool(poolName: string): number[] { + const poolKey = this.normalizePoolName(poolName) || poolName; + const poolRaces: number[] = this.structuredRaceCard?.pools?.[poolKey] || []; + if (poolRaces.length > 0) return poolRaces; + 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[poolKey] || []; } // --- MODIFIED METHOD --- @@ -462,6 +501,10 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { const poolKey = this.normalizePoolName(poolName) || poolName; const poolRaces: number[] = this.structuredRaceCard?.pools?.[poolKey] || []; let baseRaceIdx = poolRaces.length > 0 ? poolRaces[0] : this.getDefaultBaseRace(poolKey); + // Ensure baseRaceIdx is open + if (!this.isOpen(baseRaceIdx)) { + baseRaceIdx = this.getOpenRaceStartingFrom(baseRaceIdx); + } if (baseRaceIdx + maxLegs - 1 > totalRaces) { baseRaceIdx = Math.max(1, totalRaces - maxLegs + 1); } @@ -478,6 +521,26 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { return poolRaceMap[poolName.toLowerCase()] || parseInt(this.selectedRaceNumber, 10); } + private isOpen(race: number): boolean { + const status = this.stopbetStatuses.get(race); + return status === 'N' || status === undefined; // Assume open if unknown + } + + private getOpenRaceStartingFrom(start: number): number { + const max = this.structuredRaceCard?.raceVenueRaces?.races?.length || 10; + for (let r = start; r <= max; r++) { + if (this.isOpen(r)) { + return r; + } + } + for (let r = 1; r < start; r++) { + if (this.isOpen(r)) { + return r; + } + } + return start; + } + selectNumber(number: number) { if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return; @@ -1164,7 +1227,7 @@ const winLabels = allRows.map(row => { // ); // displayNumbers = displayNumbers.flatMap((n, idx, arr) => { // if (n === 'F') { -// const horses = this.getHorseNumbersForSelectedRace().map(num => num.toString()).join(','); + // const horses = this.getHorseNumbersForSelectedRace().map(num => num.toString()).join(','); // const isFirst = idx === 0; // const isLast = idx === arr.length - 1; // if (isFirst && !isLast) return [`${horses}-`]; @@ -1555,8 +1618,8 @@ try { this.erase(); // ✅ Clear selections after successful print - //--------------------Ended Print here ----------------------------- - + //--------------------Ended Print here ---------------------------- + this.selectionService.finalizeCurrentRow(); // Call after finalizeCurrentRow this.refreshBlockedLabels(null); diff --git a/btc-UI/src/app/service/stopbet.service.ts b/btc-UI/src/app/service/stopbet.service.ts new file mode 100644 index 0000000..5bcce7f --- /dev/null +++ b/btc-UI/src/app/service/stopbet.service.ts @@ -0,0 +1,94 @@ +// stopbet.service.ts +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { NgZone } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class StopbetService { + private stopbetStatuses: Map = new Map(); // raceNum => 'Y'|'N'|'S' + private stopbetStatusesSubject = new BehaviorSubject>(this.stopbetStatuses); + private currentVenue: string = ''; + private currentDate: string = ''; + private eventSource?: EventSource; + + constructor(private http: HttpClient, private zone: NgZone) {} + + // Expose stopbet statuses as observable + getStopbetStatuses(): Observable> { + return this.stopbetStatusesSubject.asObservable(); + } + + // Initialize stopbet for a venue and date + initialize(venue: string, date: string) { + this.currentVenue = venue.toUpperCase(); + this.currentDate = date; + this.fetchInitialStopbets(); + this.setupSSE(); + } + + 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] || {}; + this.stopbetStatuses.clear(); + for (const race in meeting) { + const raceNum = parseInt(race, 10); + this.stopbetStatuses.set(raceNum, meeting[race]); + } + this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); + } + }, + 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); + this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); + } + } catch (err) { + console.error('[STOPBET] SSE parse error:', err); + } + }); + }; + this.eventSource.onerror = (err) => { + console.error('[STOPBET] SSE error:', err); + }; + } + + isOpen(race: number): boolean { + const status = this.stopbetStatuses.get(race); + return status === 'N' || status === undefined; + } + + getOpenRaceStartingFrom(start: number, maxRaces: number): number { + for (let r = start; r <= maxRaces; r++) { + if (this.isOpen(r)) return r; + } + for (let r = 1; r < start; r++) { + if (this.isOpen(r)) return r; + } + return start; + } + + ngOnDestroy() { + if (this.eventSource) { + this.eventSource.close(); + } + } +} \ No newline at end of file