fix : stop bet (local storage)

This commit is contained in:
karthik 2025-09-17 13:25:14 +05:30
parent bd4f1e21d0
commit 89009d28c4
2 changed files with 292 additions and 212 deletions

View File

@ -1,4 +1,3 @@
// navbar.component.ts
import { Component, OnInit, HostListener, OnDestroy, NgZone, Inject } from '@angular/core'; import { Component, OnInit, HostListener, OnDestroy, NgZone, Inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { BtcService } from '../../service/btc.service'; import { BtcService } from '../../service/btc.service';
@ -24,7 +23,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
userName: string = ''; userName: string = '';
btid: string | null = null; btid: string | null = null;
// component properties - add these at top-level in the component class // Component properties
consecTimeouts = 0; consecTimeouts = 0;
maxConsecTimeouts = 2; maxConsecTimeouts = 2;
liveStatusOk = true; liveStatusOk = true;
@ -33,7 +32,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
showRaceModal = false; showRaceModal = false;
selectedVenue = 'Select Venue'; selectedVenue = 'Select Venue';
selectedRace: number = 1; selectedRace: number = 1;
currentLegRaceDisplay: string = ''; // Display current leg's race or pool start currentLegRaceDisplay: string = '';
showWalletModal = false; showWalletModal = false;
showResultModal = false; showResultModal = false;
@ -43,13 +42,12 @@ export class NavbarComponent implements OnInit, OnDestroy {
raceData: any[] = []; raceData: any[] = [];
objectKeys = Object.keys; objectKeys = Object.keys;
selectedRaceId: number = 0; // index into races array selectedRaceId: number = 0;
enabledHorseNumbers: number[] = []; enabledHorseNumbers: number[] = [];
multiLegBaseRaceIdx: number = 0; multiLegBaseRaceIdx: number = 0;
currentPool: string | null = null; currentPool: string | null = null;
private prevEnabledKey = ''; // For memoization private prevEnabledKey = '';
wallet = { wallet = {
withdraw: 0, withdraw: 0,
deposit: 0, deposit: 0,
@ -71,11 +69,11 @@ export class NavbarComponent implements OnInit, OnDestroy {
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.'];
// Stopbet-related properties (merged approach) // Stopbet-related properties
private stopbetStatuses: Map<number, string> = new Map(); // raceNum => 'Y'|'N'|'S' private stopbetStatuses: Map<number, string> = new Map();
private currentVenue: string = ''; private currentVenue: string = '';
private currentDate: string = ''; private currentDate: string = '';
private eventSource?: EventSource; private eventSource: EventSource | undefined;
formattedTicketLogs: { formattedTicketLogs: {
pool: string; pool: string;
@ -139,51 +137,37 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
} }
// Set current venue and date this.currentVenue = (this.raceCardData?.venue ?? '').toUpperCase() || localStorage.getItem('selectedVenue') || '';
this.currentVenue = (this.raceCardData?.venue ?? '').toUpperCase();
this.currentDate = this.getTodayDate(); this.currentDate = this.getTodayDate();
console.log('[INIT] Current venue:', this.currentVenue, 'date:', this.currentDate);
// Setup stopbet if venue is available:
// 1) initialize StopbetService (if it uses BroadcastChannel or another approach)
if (this.currentVenue) { if (this.currentVenue) {
try { try {
this.stopbetService.initialize(this.currentVenue, this.currentDate); this.stopbetService.initialize(this.currentVenue, this.currentDate);
} catch (e) { } catch (e) {
// initialization may not be necessary or supported by the service; ignore if fails console.warn('[STOPBET] stopbetService.initialize failed:', e);
console.warn('[STOPBET] stopbetService.initialize failed or not required:', e); }
} else {
console.warn('[INIT] No venue set, skipping stopbetService.initialize');
} }
// 2) fetch initial stopbets (HTTP fallback) and setup SSE stream
this.fetchInitialStopbets();
this.setupSSE();
}
// 3) subscribe to StopbetService (if it provides an observable of statuses)
try { try {
const statuses$ = (this.stopbetService as any).getStopbetStatuses?.(); const statuses$ = this.stopbetService.getStopbetStatuses();
if (statuses$ && typeof statuses$.subscribe === 'function') { if (statuses$ && typeof statuses$.subscribe === 'function') {
this.stopbetSubscription = statuses$.subscribe((statuses: any) => { this.stopbetSubscription = statuses$.subscribe((statuses: Map<number, string>) => {
// if the service gives you a map/object, merge into local stopbetStatuses this.stopbetStatuses.clear();
if (statuses && typeof statuses === 'object') { statuses.forEach((value, key) => {
try { this.stopbetStatuses.set(key, value);
// 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 { console.log('[NAVBAR] Updated stopbetStatuses:', Object.fromEntries(this.stopbetStatuses));
// ignore malformed
}
}
// Trigger check/switch if current race is stopped
this.checkAndSwitchIfStopped(); this.checkAndSwitchIfStopped();
this.updateEnabledHorseNumbers();
}); });
} }
} catch (e) { } catch (e) {
console.warn('[STOPBET] subscribe to service failed:', e); console.warn('[STOPBET] Subscribe to service failed:', e);
} }
// Periodic ABS/latest polling (unchanged)
this.subscription = interval(5000) this.subscription = interval(5000)
.pipe( .pipe(
switchMap(() => { switchMap(() => {
@ -198,15 +182,21 @@ export class NavbarComponent implements OnInit, OnDestroy {
.subscribe((res: any) => { .subscribe((res: any) => {
if (res) { if (res) {
this.liveStatusOk = res.success === true; this.liveStatusOk = res.success === true;
if (res.raceCardData) { if (res.raceCardData) {
this.raceCardData = res.raceCardData; this.raceCardData = res.raceCardData;
localStorage.setItem('raceCardData', JSON.stringify(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) { if (res.wallet) {
this.wallet = res.wallet; this.wallet = res.wallet;
} }
} }
this.updateEnabledHorseNumbers();
}); });
// Subscribe to shared state updates // Subscribe to shared state updates
@ -216,7 +206,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
if (this.currentPool) { if (this.currentPool) {
const leg = this.getLegIndexForRace(this.currentPool, data.value); const leg = this.getLegIndexForRace(this.currentPool, data.value);
this.currentLegRaceDisplay = `Leg ${leg + 1} (Race ${data.value}) for ${this.currentPool}`; this.currentLegRaceDisplay = `Leg ${leg + 1} (Race ${data.value}) for ${this.currentPool}`;
this.updateEnabledHorseNumbersForMultiLeg(this.multiLegBaseRaceIdx); this.updateEnabledHorseNumbers();
} else { } else {
this.currentLegRaceDisplay = ''; this.currentLegRaceDisplay = '';
this.updateEnabledHorseNumbers(); this.updateEnabledHorseNumbers();
@ -226,8 +216,9 @@ export class NavbarComponent implements OnInit, OnDestroy {
const { label, baseRaceIdx } = data.value; const { label, baseRaceIdx } = data.value;
this.currentPool = label; this.currentPool = label;
this.multiLegBaseRaceIdx = baseRaceIdx; this.multiLegBaseRaceIdx = baseRaceIdx;
this.selectedRace = this.getValidRaceForLeg(label, 0);
this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${label}`; this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${label}`;
this.updateEnabledHorseNumbersForMultiLeg(baseRaceIdx); this.updateEnabledHorseNumbers();
} }
if (data.type === 'multiLegPoolEnd') { if (data.type === 'multiLegPoolEnd') {
this.currentPool = null; this.currentPool = null;
@ -242,6 +233,15 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.selectedRace = data.value; this.selectedRace = data.value;
this.updateEnabledHorseNumbers(); 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);
}
}
}); });
} }
@ -253,70 +253,8 @@ export class NavbarComponent implements OnInit, OnDestroy {
return `${year}/${month}/${day}`; 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 && 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]);
}
// After loading, ensure current selected race is valid
this.checkAndSwitchIfStopped();
}
},
error: (err) => {
console.error('[STOPBET] Failed to fetch initial statuses:', err);
}
});
}
/**
* SSE stream setup for real-time stopbet updates.
*/
private setupSSE() {
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);
}
});
};
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 { private isOpen(race: number): boolean {
// Prefer explicit map; if unknown assume open return this.stopbetService.isOpen(race);
const status = this.stopbetStatuses.get(race);
return status === 'N' || status === undefined;
} }
private getMaxRaces(): number { private getMaxRaces(): number {
@ -325,54 +263,116 @@ export class NavbarComponent implements OnInit, OnDestroy {
private getOpenRaceStartingFrom(start: number) { private getOpenRaceStartingFrom(start: number) {
const max = this.getMaxRaces(); const max = this.getMaxRaces();
for (let r = start; r <= max; r++) { return this.stopbetService.getOpenRaceStartingFrom(start, max);
if (this.isOpen(r)) {
return r;
}
}
for (let r = 1; r < start; r++) {
if (this.isOpen(r)) {
return r;
}
}
return start;
} }
private checkAndSwitchIfStopped() { private checkAndSwitchIfStopped() {
if (!this.isOpen(this.selectedRace)) { if (!this.isOpen(this.selectedRace)) {
const nextOpen = this.getOpenRaceStartingFrom(this.selectedRace + 1); const nextOpen = this.getOpenRaceStartingFrom(this.selectedRace + 1);
if (nextOpen !== this.selectedRace) { if (nextOpen !== this.selectedRace) {
console.log('[NAVBAR] Switching from stopped race', this.selectedRace, 'to', nextOpen);
this.selectRace(nextOpen); this.selectRace(nextOpen);
} }
} }
} }
private getLegIndexForRace(poolName: string, race: number): number { private getLegIndexForRace(poolName: string | null, race: number): number {
if (!poolName) return 0;
const raceMap: { [key: string]: number[] } = { const raceMap: { [key: string]: number[] } = {
mjp1: [1, 2, 3, 4], mjp1: [1, 2, 3, 4],
jkp1: [3, 4, 5, 6, 7], jkp1: [3, 4, 5, 6, 7],
trb1: [2, 3, 4], trb1: [2, 3, 4],
trb2: [5, 6, 7], trb2: [5, 6, 7],
}; };
return raceMap[poolName]?.indexOf(race) ?? 0; return raceMap[poolName.toLowerCase()]?.indexOf(race) ?? 0;
} }
private updateEnabledHorseNumbersForMultiLeg(baseRaceIdx: number) { private getRaceForLeg(poolName: string | null, leg: number): number {
// Defensive read of races array (structured or legacy) if (!poolName) return this.multiLegBaseRaceIdx + leg;
const racesArr = this.raceCardData?.raceVenueRaces?.races ?? []; const raceMap: { [key: string]: number[] } = {
const legCount = this.getLegCountForLabel(); mjp1: [1, 2, 3, 4],
const key = `${this.currentPool}-${baseRaceIdx}-${legCount}`; jkp1: [3, 4, 5, 6, 7],
trb1: [2, 3, 4],
trb2: [5, 6, 7],
};
return raceMap[poolName.toLowerCase()]?.[leg] ?? this.multiLegBaseRaceIdx + leg;
}
if (this.prevEnabledKey === key) return; // memoization 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; 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[] = []; let combinedHorseNumbers: number[] = [];
const raceIndices = Array.from({ length: legCount }, (_, i) => this.getRaceForLeg(this.currentPool || '', i) - 1); for (let leg = 0; leg < legCount; leg++) {
const raceNum = validRaces[leg];
if (!raceNum || !this.isOpen(raceNum)) continue;
for (const raceIdx of raceIndices) { const raceIdx = raceNum - 1;
const rawRace = racesArr[raceIdx] ?? []; const rawRace = racesArr[raceIdx] ?? [];
let runners: any[] = [];
if (Array.isArray(rawRace)) { if (Array.isArray(rawRace)) {
runners = rawRace; runners = rawRace;
@ -384,41 +384,34 @@ export class NavbarComponent implements OnInit, OnDestroy {
const horses = runners const horses = runners
.map((runner: any) => runner?.horseNumber ?? runner?.number ?? runner?.horse_no) .map((runner: any) => runner?.horseNumber ?? runner?.number ?? runner?.horse_no)
.filter((n: number) => typeof n === 'number' && n >= 1 && n <= 30); .filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30);
combinedHorseNumbers.push(...horses); combinedHorseNumbers.push(...horses);
} }
this.enabledHorseNumbers = Array.from(new Set(combinedHorseNumbers)); 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({ this.sharedStateService.updateSharedData({
type: 'enabledHorseNumbers', type: 'enabledHorseNumbers',
value: this.enabledHorseNumbers, value: this.enabledHorseNumbers,
}); });
} console.log('[NAVBAR] Updated enabledHorseNumbers:', this.enabledHorseNumbers);
private getLegCountForLabel(): number {
switch (this.currentPool) {
case 'mjp1':
return 4;
case 'jkp1':
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;
} }
updateDateTime() { updateDateTime() {
@ -496,9 +489,8 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.currentVenue = newVenue; this.currentVenue = newVenue;
this.stopbetStatuses.clear(); this.stopbetStatuses.clear();
if (this.currentVenue) { if (this.currentVenue) {
this.fetchInitialStopbets();
this.setupSSE();
try { try {
console.log('[NAVBAR] Initializing stopbetService for venue:', newVenue);
this.stopbetService.initialize(this.currentVenue, this.currentDate); this.stopbetService.initialize(this.currentVenue, this.currentDate);
} catch {} } catch {}
} }
@ -540,35 +532,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.closeModals(); this.closeModals();
} }
updateEnabledHorseNumbers() {
const raceIndex = this.selectedRace - 1;
const racesArr = this.raceCardData?.raceVenueRaces?.races ?? [];
const rawRace = racesArr?.[raceIndex];
let runners: any[] = [];
if (Array.isArray(rawRace)) {
runners = rawRace;
} else if (rawRace && Array.isArray(rawRace.runners)) {
runners = rawRace.runners;
} else {
runners = [];
}
if (Array.isArray(runners)) {
this.enabledHorseNumbers = runners
.map((r: any) => r?.horseNumber ?? r?.number ?? r?.horse_no)
.filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30);
} else {
this.enabledHorseNumbers = [];
}
this.sharedStateService.updateSharedData({
type: 'enabledHorseNumbers',
value: this.enabledHorseNumbers,
});
}
selectHorseNumber(number: number) { selectHorseNumber(number: number) {
// Intentionally left no-op (UI hook) // Intentionally left no-op (UI hook)
} }
@ -625,7 +588,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
const horsesPartMatch = rawLabel.match(/^\w+\s+(.+?)\s+\*\d+/); const horsesPartMatch = rawLabel.match(/^\w+\s+(.+?)\s+\*\d+/);
if (horsesPartMatch) { if (horsesPartMatch) {
horses = horsesPartMatch[1].trim(); horses = horsesPartMatch[1].trim();
// Special pools split into races // Special pools split into races
if (['MJP', 'JKP', 'TRE'].includes(pool)) { if (['MJP', 'JKP', 'TRE'].includes(pool)) {
horsesArray = horses.split('/').map((r) => r.trim().split(',').map((h) => h.trim())); horsesArray = horses.split('/').map((r) => r.trim().split(',').map((h) => h.trim()));
@ -665,6 +627,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
logout(): void { logout(): void {
const name = localStorage.getItem('userName') || 'Unknown User'; const name = localStorage.getItem('userName') || 'Unknown User';
const employeeId = localStorage.getItem('employeeId') || '000000'; const employeeId = localStorage.getItem('employeeId') || '000000';
const stopbetData = localStorage.getItem('stopbetStatuses'); // Save stopbetStatuses
const printData = { const printData = {
name, name,
@ -682,11 +645,20 @@ export class NavbarComponent implements OnInit, OnDestroy {
if (!res.ok) throw new Error('Logout print failed'); if (!res.ok) throw new Error('Logout print failed');
(window as any).electronAPI?.closeSecondScreen?.(); (window as any).electronAPI?.closeSecondScreen?.();
localStorage.clear(); localStorage.clear();
if (stopbetData) {
localStorage.setItem('stopbetStatuses', stopbetData);
console.log('[NAVBAR] Preserved stopbetStatuses in localStorage:', stopbetData);
}
this.router.navigate(['/logout']); this.router.navigate(['/logout']);
}) })
.catch((err) => { .catch((err) => {
console.error('[NAVBAR] Logout error:', err);
(window as any).electronAPI?.closeSecondScreen?.(); (window as any).electronAPI?.closeSecondScreen?.();
localStorage.clear(); localStorage.clear();
if (stopbetData) {
localStorage.setItem('stopbetStatuses', stopbetData);
console.log('[NAVBAR] Preserved stopbetStatuses in localStorage:', stopbetData);
}
this.router.navigate(['/logout']); this.router.navigate(['/logout']);
}); });
} }

View File

@ -1,4 +1,3 @@
// stopbet.service.ts
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
@ -11,6 +10,7 @@ export class StopbetService {
private currentVenue: string = ''; private currentVenue: string = '';
private currentDate: string = ''; private currentDate: string = '';
private eventSource?: EventSource; private eventSource?: EventSource;
private readonly STORAGE_KEY = 'stopbetStatuses';
constructor(private http: HttpClient, private zone: NgZone) {} constructor(private http: HttpClient, private zone: NgZone) {}
@ -21,14 +21,109 @@ export class StopbetService {
// Initialize stopbet for a venue and date // Initialize stopbet for a venue and date
initialize(venue: string, date: string) { initialize(venue: string, date: string) {
this.currentVenue = venue.toUpperCase(); const newVenue = venue ? venue.toUpperCase() : '';
this.currentDate = date; const newDate = date || this.getTodayDate();
// Only clear if venue and date are valid and different
if (newVenue && newDate && (this.currentVenue !== newVenue || this.currentDate !== newDate)) {
console.log(
`[STOPBET] Venue or date changed (from ${this.currentVenue}:${this.currentDate} to ${newVenue}:${newDate}), clearing stopbetStatuses`
);
this.stopbetStatuses.clear();
localStorage.removeItem(this.STORAGE_KEY);
this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses));
} else {
console.log(
`[STOPBET] No clear needed: newVenue=${newVenue}, newDate=${newDate}, currentVenue=${this.currentVenue}, currentDate=${this.currentDate}`
);
}
this.currentVenue = newVenue;
this.currentDate = newDate;
// Load from localStorage first
this.loadFromLocalStorage();
// Fetch from server and setup SSE
this.fetchInitialStopbets(); this.fetchInitialStopbets();
this.setupSSE(); this.setupSSE();
} }
// Public method to set race status
setStatus(raceNum: number, status: string) {
const oldStatus = this.stopbetStatuses.get(raceNum);
if (oldStatus === status) {
console.log(`[STOPBET] No change for race ${raceNum}: status remains ${status}`);
return;
}
// Set the specified race status
this.stopbetStatuses.set(raceNum, status);
// If stopping this race, auto-disable previous races
if (status === 'Y' || status === 'S') {
for (let prevRace = 1; prevRace < raceNum; prevRace++) {
const prevStatus = this.stopbetStatuses.get(prevRace);
if (prevStatus === 'N' || prevStatus === undefined) {
console.log(`[STOPBET] Auto-stopping race ${prevRace} due to race ${raceNum} being stopped`);
this.stopbetStatuses.set(prevRace, 'Y');
}
}
}
// Emit updated statuses and save to localStorage
this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses));
this.saveToLocalStorage();
}
private loadFromLocalStorage() {
const cached = localStorage.getItem(this.STORAGE_KEY);
if (cached) {
try {
const parsed = JSON.parse(cached);
if (parsed.venue === this.currentVenue && parsed.date === this.currentDate) {
this.stopbetStatuses.clear();
for (const [race, status] of Object.entries(parsed.statuses)) {
const raceNum = parseInt(race, 10);
if (!isNaN(raceNum) && ['Y', 'N', 'S'].includes(status as string)) {
this.stopbetStatuses.set(raceNum, status as string);
}
}
console.log('[STOPBET] Loaded statuses from localStorage:', this.stopbetStatuses);
this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses));
} else {
console.log(
`[STOPBET] localStorage data invalid for venue=${this.currentVenue}, date=${this.currentDate}:`,
parsed
);
}
} catch (err) {
console.error('[STOPBET] Failed to parse localStorage:', err);
localStorage.removeItem(this.STORAGE_KEY);
}
} else {
console.log('[STOPBET] No stopbetStatuses found in localStorage');
}
}
private saveToLocalStorage() {
try {
const data = {
venue: this.currentVenue,
date: this.currentDate,
statuses: Object.fromEntries(this.stopbetStatuses),
};
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
console.log('[STOPBET] Saved statuses to localStorage:', data);
} catch (err) {
console.error('[STOPBET] Failed to save to localStorage:', err);
}
}
private fetchInitialStopbets() { private fetchInitialStopbets() {
this.http.get(`http://localhost:8080/stopbet/raw?venue=${this.currentVenue}&date=${this.currentDate}`).subscribe({ this.http
.get(`http://localhost:8080/stopbet/raw?venue=${this.currentVenue}&date=${this.currentDate}`)
.subscribe({
next: (res: any) => { next: (res: any) => {
if (res.ok && res.data) { if (res.ok && res.data) {
const key = `${this.currentVenue}:${this.currentDate}`; const key = `${this.currentVenue}:${this.currentDate}`;
@ -36,14 +131,19 @@ export class StopbetService {
this.stopbetStatuses.clear(); this.stopbetStatuses.clear();
for (const race in meeting) { for (const race in meeting) {
const raceNum = parseInt(race, 10); const raceNum = parseInt(race, 10);
this.stopbetStatuses.set(raceNum, meeting[race]); const status = meeting[race];
this.setStatus(raceNum, status);
} }
console.log('[STOPBET] Fetched initial statuses:', this.stopbetStatuses);
this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses));
this.saveToLocalStorage();
} else {
console.warn('[STOPBET] Invalid response from fetchInitialStopbets:', res);
} }
}, },
error: (err) => { error: (err) => {
console.error('[STOPBET] Failed to fetch initial statuses:', err); console.error('[STOPBET] Failed to fetch initial statuses:', err);
} },
}); });
} }
@ -58,8 +158,8 @@ export class StopbetService {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.type === 'stopbet' && data.venue === this.currentVenue && data.date === this.currentDate) { if (data.type === 'stopbet' && data.venue === this.currentVenue && data.date === this.currentDate) {
const raceNum = parseInt(data.race, 10); const raceNum = parseInt(data.race, 10);
this.stopbetStatuses.set(raceNum, data.status); this.setStatus(raceNum, data.status);
this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); console.log('[STOPBET] SSE updated race', raceNum, 'to', data.status);
} }
} catch (err) { } catch (err) {
console.error('[STOPBET] SSE parse error:', err); console.error('[STOPBET] SSE parse error:', err);
@ -86,6 +186,14 @@ export class StopbetService {
return start; return start;
} }
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}`;
}
ngOnDestroy() { ngOnDestroy() {
if (this.eventSource) { if (this.eventSource) {
this.eventSource.close(); this.eventSource.close();