btc_horse/btc-UI/src/app/components/navbar/navbar.component.ts

772 lines
25 KiB
TypeScript
Executable File

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