fix : added service for the stop bet

This commit is contained in:
karthik 2025-09-15 16:29:46 +05:30
parent d851b0f949
commit 5984f9d036
3 changed files with 297 additions and 114 deletions

View File

@ -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<number, string> = 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<number, string> = 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;
}
}
}

View File

@ -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<number, string> = 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<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 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);

View File

@ -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<number, string> = new Map(); // raceNum => 'Y'|'N'|'S'
private stopbetStatusesSubject = new BehaviorSubject<Map<number, string>>(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<Map<number, string>> {
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();
}
}
}