feat : issue tickets to bakend from GUI

This commit is contained in:
Sibin Sabu 2025-09-14 13:51:18 +05:30
parent b680150a88
commit 6d752fcfb2
3 changed files with 641 additions and 213 deletions

View File

@ -1,6 +1,7 @@
import { Component, OnInit, HostListener, OnDestroy, NgZone } from '@angular/core'; import { Component, OnInit, HostListener, OnDestroy, NgZone } 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';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { catchError, interval, of, Subscription, switchMap } from 'rxjs'; import { catchError, interval, of, Subscription, switchMap } from 'rxjs';
import { SharedStateService } from '../../service/shared-state.service'; import { SharedStateService } from '../../service/shared-state.service';
@ -10,7 +11,7 @@ import { SharedStateService } from '../../service/shared-state.service';
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.css'], styleUrls: ['./navbar.component.css'],
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, HttpClientModule],
}) })
export class NavbarComponent implements OnInit, OnDestroy { export class NavbarComponent implements OnInit, OnDestroy {
dateTime: string = ''; dateTime: string = '';
@ -19,32 +20,52 @@ export class NavbarComponent implements OnInit, OnDestroy {
private subscription!: Subscription; private subscription!: Subscription;
userName: string = ''; userName: string = '';
btid: string | null = null; btid: string | null = null;
liveStatusOk: boolean = true;
// component properties - add these at top-level in the component class
consecTimeouts = 0;
maxConsecTimeouts = 2;
liveStatusOk = true;
showVenueModal = false; showVenueModal = false;
showRaceModal = false; showRaceModal = false;
selectedVenue = 'Select Venue'; selectedVenue = 'Select Venue';
selectedRace: number = 1; selectedRace: number = 1;
currentLegRaceDisplay: string = ''; currentLegRaceDisplay: string = ''; // Display current leg's race or pool start
showWalletModal = false; showWalletModal = false;
showResultModal = false; showResultModal = false;
showMessagesModal = false; showMessagesModal = false;
showLogModal = false; showLogModal = false;
raceCardData: any = null; raceCardData: any = {};
raceData: any[] = []; raceData: any[] = [];
objectKeys = Object.keys; objectKeys = Object.keys;
selectedRaceId: number = 0; selectedRaceId: number = 0; // index into races array
enabledHorseNumbers: number[] = []; enabledHorseNumbers: number[] = [];
multiLegBaseRaceIdx: number = 0; multiLegBaseRaceIdx: number = 0;
currentPool: string | null = null; currentPool: string | null = null;
private prevEnabledKey = ''; private prevEnabledKey = ''; // For memoization
wallet = { withdraw: 0, deposit: 0, payout: 0, cancel: 0, ticketing: 0, balance: 0 }; wallet = {
withdraw: 0,
deposit: 0,
payout: 0,
cancel: 0,
ticketing: 0,
balance: 0,
};
logs = [{ description: '', venue: '', ticketNumber: '', poolName: '', totalAmount: '' }]; logs = [
{
description: '',
venue: '',
ticketNumber: '',
poolName: '',
totalAmount: '',
},
];
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.'];
@ -52,28 +73,83 @@ export class NavbarComponent implements OnInit, OnDestroy {
private btcService: BtcService, private btcService: BtcService,
private router: Router, private router: Router,
private sharedStateService: SharedStateService, private sharedStateService: SharedStateService,
private zone: NgZone private zone: NgZone,
private http: HttpClient
) {} ) {}
ngOnInit() { ngOnInit() {
this.userName = localStorage.getItem('userName') || ''; this.userName = localStorage.getItem('userName') || '';
this.btid = localStorage.getItem('btid'); this.btid = localStorage.getItem('btid');
// Use NgZone to run setInterval outside Angular's change detection // Use NgZone to run setInterval outside Angular's change detection
this.zone.runOutsideAngular(() => { this.zone.runOutsideAngular(() => {
setInterval(() => { this.zone.run(() => this.updateDateTime()); }, 1000); setInterval(() => {
this.zone.run(() => this.updateDateTime());
}, 1000);
}); });
// === Load structuredRaceCard from localdb (rpinfo) if available ===
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 = {};
}
}
// Periodic ABS/latest polling
this.subscription = interval(5000) this.subscription = interval(5000)
.pipe( .pipe(
switchMap(() => switchMap(() => {
this.btcService.pingLiveStatus().pipe( console.log(`[ANGULAR] Fetching latest ABS status at ${new Date().toISOString()}`);
catchError((error) => { console.error('[LIVE STATUS] Ping failed:', error); this.liveStatusOk = false; return of(null); }) return this.http.get('http://localhost:8080/abs/latest').pipe(
)) catchError((err) => {
).subscribe((response) => { console.error('[ANGULAR] ABS latest fetch failed ❌', err);
if (response !== null) { this.liveStatusOk = true; console.log('[LIVE STATUS] OK'); } this.liveStatusOk = false;
return of(null);
})
);
})
)
.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;
}
}
}); });
this.sharedStateService.sharedData$.subscribe(data => { // Subscribe to shared state updates
this.sharedStateService.sharedData$.subscribe((data) => {
if (data.type === 'currentLegRace') { if (data.type === 'currentLegRace') {
this.selectedRace = data.value; this.selectedRace = data.value;
if (this.currentPool) { if (this.currentPool) {
@ -87,11 +163,11 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
if (data.type === 'multiLegPoolStart') { if (data.type === 'multiLegPoolStart') {
const { label, baseRaceIdx } = data.value; const { label, baseRaceIdx } = data.value;
this.currentPool = this.normalizePoolName(label); this.currentPool = label;
this.multiLegBaseRaceIdx = baseRaceIdx; this.multiLegBaseRaceIdx = baseRaceIdx;
this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${this.currentPool}`; this.currentLegRaceDisplay = `Starting at Race ${baseRaceIdx} for ${label}`;
this.updateEnabledHorseNumbersForMultiLeg(baseRaceIdx); this.updateEnabledHorseNumbersForMultiLeg(baseRaceIdx);
console.log(`[Multi-leg Pool] Selected: ${this.currentPool}, Base Race: ${baseRaceIdx}`); console.log(`[Multi-leg Pool] Selected: ${label}, Base Race: ${baseRaceIdx}`);
} }
if (data.type === 'multiLegPoolEnd') { if (data.type === 'multiLegPoolEnd') {
this.currentPool = null; this.currentPool = null;
@ -107,115 +183,135 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.updateEnabledHorseNumbers(); this.updateEnabledHorseNumbers();
} }
}); });
this.loadRaceCardData();
}
private safeGetJSON(key: string): any | null {
try {
const raw = localStorage.getItem(key);
if (!raw) return null;
return JSON.parse(raw);
} catch (err) {
console.warn(`[safeGetJSON] failed parse ${key}`, err);
return null;
}
}
private loadRaceCardData() {
const cached = this.safeGetJSON('rpinfo');
if (cached && cached.structuredRaceCard) {
this.raceCardData = cached.structuredRaceCard;
this.selectedVenue = this.raceCardData?.venue || 'Select Venue';
this.updateEnabledHorseNumbers();
return;
}
const rc = this.safeGetJSON('raceCardData');
if (rc) {
this.raceCardData = rc;
this.selectedVenue = this.raceCardData?.venue || 'Select Venue';
this.updateEnabledHorseNumbers();
return;
}
this.raceCardData = { error: 'Race card not available locally' };
this.selectedVenue = 'Select Venue';
}
private normalizePoolName(name: string | null | undefined): string | null {
if (!name) return null;
const n = String(name).toLowerCase();
if (n.startsWith('trb') || n.startsWith('tbp')) return n.includes('2') ? 'trb2' : 'trb1';
if (n.startsWith('mjp')) return n.includes('2') ? 'mjp2' : 'mjp1';
if (n.startsWith('jkp') || n.startsWith('jpp') || n.startsWith('jk')) return n.includes('2') ? 'jkp2' : 'jkp1';
return n;
} }
private getLegIndexForRace(poolName: string, race: number): number { private getLegIndexForRace(poolName: string, race: number): number {
if (!this.raceCardData) return 0; const raceMap: { [key: string]: number[] } = {
const normalized = this.normalizePoolName(poolName) || poolName; mjp1: [1, 2, 3, 4],
const poolMap: { [key: string]: number[] } = this.raceCardData?.pools || {}; jkp1: [3, 4, 5, 6, 7],
const arr = poolMap[normalized] || poolMap[poolName] || []; trb1: [2, 3, 4],
return arr.indexOf(race) >= 0 ? arr.indexOf(race) : 0; trb2: [5, 6, 7],
};
return raceMap[poolName]?.indexOf(race) ?? 0;
} }
private updateEnabledHorseNumbersForMultiLeg(baseRaceIdx: number) { private updateEnabledHorseNumbersForMultiLeg(baseRaceIdx: number) {
const racesArr = this.raceCardData?.raceVenueRaces?.races || []; // Defensive read of races array (structured or legacy)
const racesArr = this.raceCardData?.raceVenueRaces?.races ?? [];
const legCount = this.getLegCountForLabel(); const legCount = this.getLegCountForLabel();
const poolKey = this.currentPool || ''; const key = `${this.currentPool}-${baseRaceIdx}-${legCount}`;
const key = `${poolKey}-${baseRaceIdx}-${legCount}`;
if (this.prevEnabledKey === key) return; if (this.prevEnabledKey === key) return; // memoization
this.prevEnabledKey = key; this.prevEnabledKey = key;
let combinedHorseNumbers: number[] = []; let combinedHorseNumbers: number[] = [];
const raceIndices = Array.from({ length: legCount }, (_, i) => this.getRaceForLeg(poolKey, i) - 1);
const raceIndices = Array.from({ length: legCount }, (_, i) => this.getRaceForLeg(this.currentPool || '', i) - 1);
for (const raceIdx of raceIndices) { for (const raceIdx of raceIndices) {
const race = racesArr[raceIdx] || {}; const rawRace = racesArr[raceIdx] ?? [];
if (Array.isArray(race.horses)) { let runners: any[] = [];
const horses = race.horses.filter((n: any) => typeof n === 'number' && n >= 1 && n <= 30);
combinedHorseNumbers.push(...horses); 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: number) => typeof n === 'number' && n >= 1 && n <= 30);
combinedHorseNumbers.push(...horses);
} }
this.enabledHorseNumbers = Array.from(new Set(combinedHorseNumbers)); this.enabledHorseNumbers = Array.from(new Set(combinedHorseNumbers));
this.sharedStateService.updateSharedData({ type: 'enabledHorseNumbers', value: this.enabledHorseNumbers });
this.sharedStateService.updateSharedData({
type: 'enabledHorseNumbers',
value: this.enabledHorseNumbers,
});
console.log('[Multi-leg Pool] Updated enabled horse numbers:', this.enabledHorseNumbers); console.log('[Multi-leg Pool] Updated enabled horse numbers:', this.enabledHorseNumbers);
} }
private getLegCountForLabel(): number { private getLegCountForLabel(): number {
const p = (this.currentPool || '').toLowerCase(); switch (this.currentPool) {
switch (p) { case 'mjp1':
case 'mjp1': case 'mjp2': return 4; return 4;
case 'jkp1': case 'jkp2': return 5; case 'jkp1':
case 'trb1': case 'trb2': return 3; return 5;
default: return 3; case 'trb1':
case 'trb2':
return 3;
default:
return 3;
} }
} }
private getRaceForLeg(poolName: string, leg: number): number { private getRaceForLeg(poolName: string, leg: number): number {
const normalized = this.normalizePoolName(poolName) || poolName; const raceMap: { [key: string]: number[] } = {
const poolMap: { [key: string]: number[] } = this.raceCardData?.pools || {}; mjp1: [1, 2, 3, 4],
const arr = poolMap[normalized] || poolMap[poolName] || []; jkp1: [3, 4, 5, 6, 7],
if (arr.length > leg) return arr[leg]; trb1: [2, 3, 4],
return (this.multiLegBaseRaceIdx || 1) + leg; trb2: [5, 6, 7],
};
return raceMap[poolName]?.[leg] ?? this.multiLegBaseRaceIdx + leg;
} }
updateDateTime() { this.dateTime = new Date().toLocaleString(); } updateDateTime() {
const now = new Date();
this.dateTime = now.toLocaleString();
}
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(event: any) { onResize(event: any) {
this.screenWidth = event.target.innerWidth; this.screenWidth = event.target.innerWidth;
if (this.screenWidth > 800) this.isMenuOpen = false; if (this.screenWidth > 800) {
this.isMenuOpen = false;
}
} }
toggleMenu() { this.isMenuOpen = !this.isMenuOpen; } toggleMenu() {
this.isMenuOpen = !this.isMenuOpen;
}
openVenueModal() { this.loadRaceCardData(); this.showVenueModal = true; } openVenueModal() {
// Prefer structuredRaceCard loaded at init (rpinfo). If empty, try legacy key.
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: [] }, pools: {} };
} catch {
this.raceCardData = { raceVenueRaces: { races: [] }, pools: {} };
}
} else {
this.raceCardData = { raceVenueRaces: { races: [] }, pools: {} };
}
}
// 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() { openRaceModal() {
console.log('[MODAL] Opening race modal');
this.showRaceModal = true; this.showRaceModal = true;
const race = this.raceCardData?.raceVenueRaces?.races?.[this.selectedRaceId];
this.raceData = race ? (race.horses || []) : []; 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() { closeModals() {
@ -229,9 +325,17 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
selectVenue(index: number) { selectVenue(index: number) {
this.selectedVenue = this.raceCardData?.venue || 'Unknown Venue'; // 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; this.selectedRaceId = index;
this.sharedStateService.updateSharedData({ type: 'selectedVenue', value: this.selectedVenue });
this.sharedStateService.updateSharedData({
type: 'selectedVenue',
value: this.selectedVenue,
});
console.log('[VENUE] Venue resolved to (structured):', this.selectedVenue, '| index:', index);
this.closeModals(); this.closeModals();
} }
@ -240,84 +344,196 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.currentPool = null; this.currentPool = null;
this.multiLegBaseRaceIdx = 0; this.multiLegBaseRaceIdx = 0;
this.currentLegRaceDisplay = ''; this.currentLegRaceDisplay = '';
this.sharedStateService.updateSharedData({ type: 'selectedRace', value: this.selectedRace });
const raceData = this.raceCardData?.raceVenueRaces?.races?.[race - 1] || {}; this.sharedStateService.updateSharedData({
const runnerCount = Array.isArray(raceData.horses) ? raceData.horses.length : (raceData.runners?.length || 12); type: 'selectedRace',
value: this.selectedRace,
});
// Use this.raceCardData (structured) rather than re-parsing localStorage
const raceList = this.raceCardData?.raceVenueRaces?.races ?? [];
const selectedRaceEntry = raceList[race - 1] ?? [];
// Determine runnerCount defensively based on possible shapes
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.sharedStateService.setRunnerCount(runnerCount);
this.updateEnabledHorseNumbers(); this.updateEnabledHorseNumbers();
console.log('[RACE] Race selected (structured):', this.selectedRace, '| Runner count:', runnerCount);
this.closeModals(); this.closeModals();
} }
updateEnabledHorseNumbers() { updateEnabledHorseNumbers() {
const raceIndex = this.selectedRace - 1; const raceIndex = this.selectedRace - 1;
const race = this.raceCardData?.raceVenueRaces?.races?.[raceIndex] || {}; const racesArr = this.raceCardData?.raceVenueRaces?.races ?? [];
let horses: any[] = []; const rawRace = racesArr?.[raceIndex];
if (Array.isArray(race?.horses)) horses = race.horses; let runners: any[] = [];
else if (Array.isArray(race?.runners)) horses = race.runners.map((r: any) => r.number ?? r);
else if (Array.isArray(race)) horses = race;
this.enabledHorseNumbers = horses.map((n: any) => Number(n)).filter((n: number) => !Number.isNaN(n) && n >= 1 && n <= 30); if (Array.isArray(rawRace)) {
this.sharedStateService.updateSharedData({ type: 'enabledHorseNumbers', value: this.enabledHorseNumbers }); runners = rawRace;
console.log('[HORSE NUMBERS] Enabled horse numbers:', this.enabledHorseNumbers); } 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,
});
console.log('[HORSE NUMBERS] Enabled horse numbers (structured):', this.enabledHorseNumbers);
} }
selectHorseNumber(number: number) { console.log('[HORSE] Selected horse number:', number); } selectHorseNumber(number: number) {
console.log('[HORSE] Selected horse number:', number);
}
openWalletModal() { this.showWalletModal = true; } openWalletModal() {
openResultModal() { this.showResultModal = true; } console.log('[MODAL] Opening wallet modal');
openMessagesModal() { this.showMessagesModal = true; } this.showWalletModal = true;
openLogModal() { this.showLogModal = true; } }
formattedTicketLogs: any[] = []; 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() { openViewLog() {
const storedTickets = localStorage.getItem('localTicketsViewlog'); const storedTickets = localStorage.getItem('localTicketsViewlog');
if (!storedTickets) { this.formattedTicketLogs = []; this.showLogModal = true; return; }
const tickets = JSON.parse(storedTickets);
this.formattedTicketLogs = tickets.map((ticket: any) => {
const rawLabel = ticket.winLabels?.trim() || '';
const barcodeId = ticket.barcodeId || '';
let pool = '', horses = '', horsesArray: string[][] = [], ticketCountLabel = '', price = '', maskedBarcode = '', displayBarcode = '';
if (rawLabel) { if (storedTickets) {
const parts = rawLabel.split(/\s+/); const tickets = JSON.parse(storedTickets);
pool = parts[0];
const countMatch = rawLabel.match(/\*(\d+)(?:\s+(Rs\s*\d+))?/); this.formattedTicketLogs = tickets.map((ticket: any, index: number) => {
if (countMatch) { ticketCountLabel = `*${countMatch[1]}`; price = countMatch[2] || ''; } const rawLabel = ticket.winLabels?.trim() || '';
const horsesPartMatch = rawLabel.match(/^\w+\s+(.+?)\s+\*\d+/); const numbers = ticket.numbers || [];
if (horsesPartMatch) { const count = ticket.ticketCount || 0;
horses = horsesPartMatch[1].trim(); const amount = ticket.totalAmount || 0;
if (['MJP', 'JKP', 'TRE', 'trb1', 'trb2', 'mjp1', 'jkp1'].includes(pool)) { const barcodeId = ticket.barcodeId || '';
horsesArray = horses.split('/').map((r: string) => r.trim().split(',').map(h => h.trim())); let pool = '',
} else { horses = '',
horsesArray = [horses.split(',').map(h => h.trim())]; horsesArray: string[][] = [],
ticketCountLabel = '',
price = '',
maskedBarcode = '',
displayBarcode = '';
if (rawLabel) {
// 1⃣ Extract pool (first word)
const parts = rawLabel.split(/\s+/);
pool = parts[0];
// 2⃣ Extract ticket count (*n) & price if exists
const countMatch = rawLabel.match(/\*(\d+)(?:\s+(Rs\s*\d+))?/);
if (countMatch) {
ticketCountLabel = `*${countMatch[1]}`;
price = countMatch[2] || '';
}
// 3⃣ Extract horses part (between pool name & ticket count)
const horsesPartMatch = rawLabel.match(/^\w+\s+(.+?)\s+\*\d+/);
if (horsesPartMatch) {
horses = horsesPartMatch[1].trim();
// Special pools split into races
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) { if (barcodeId) {
const last4 = barcodeId.slice(-4); const last4 = barcodeId.slice(-4);
maskedBarcode = btoa(barcodeId.slice(0, -4)); const encryptedPart = btoa(barcodeId.slice(0, -4));
displayBarcode = '********' + last4; maskedBarcode = encryptedPart;
} displayBarcode = '********' + last4;
}
return { pool, horses, horsesArray, ticketCountLabel, price, numbers: ticket.numbers || [], count: ticket.ticketCount || 0, amount: ticket.totalAmount || 0, maskedBarcode, displayBarcode }; console.log(maskedBarcode);
}); console.log('Decoded:', atob(maskedBarcode));
return {
pool,
horses,
horsesArray,
ticketCountLabel,
price,
numbers,
count,
amount,
maskedBarcode,
displayBarcode,
};
});
} else {
console.log('No tickets found in localStorage.');
this.formattedTicketLogs = [];
}
this.showLogModal = true; this.showLogModal = true;
console.log('Log modal opened. Final formattedTicketLogs:', this.formattedTicketLogs);
} }
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 printData = { name, employeeId, action: 'logout', type: 'logout' };
console.log('[LOGOUT] Initiating logout with printData:', printData);
const printData = {
name,
employeeId,
action: 'logout',
type: 'logout', // This is the missing piece
};
console.log('[LOGOUT] Initiating logout with printData:', printData);
fetch('http://localhost:9100/print', { fetch('http://localhost:9100/print', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(printData), method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(printData),
}) })
.then((res) => { .then((res) => {
if (!res.ok) throw new Error('Logout print failed'); if (!res.ok) throw new Error('Logout print failed');
@ -335,8 +551,13 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.subscription) this.subscription.unsubscribe(); if (this.subscription) {
this.subscription.unsubscribe();
}
} }
trackByHorse(index: number, item: number): number { return item; } // Add trackByHorse for use in *ngFor
trackByHorse(index: number, item: number): number {
return item;
}
} }

View File

@ -59,10 +59,10 @@ export class SelectionService {
if ((numbers.length > 0 || numbers.includes('F')) && value > 0 && label) { if ((numbers.length > 0 || numbers.includes('F')) && value > 0 && label) {
switch (label) { switch (label) {
case 'WIN': case 'WNP':
case 'SHP': case 'SHP':
case 'THP': case 'THP':
case 'PLC': case 'PLP':
case 'SHW': case 'SHW':
case 'WSP': { case 'WSP': {
updated.total = numbers.includes('F') updated.total = numbers.includes('F')
@ -71,7 +71,7 @@ export class SelectionService {
break; break;
} }
case 'FOR': { case 'FRP': {
const dashIndex = numbers.indexOf('-'); const dashIndex = numbers.indexOf('-');
const group1 = dashIndex === -1 ? numbers : numbers.slice(0, dashIndex); const group1 = dashIndex === -1 ? numbers : numbers.slice(0, dashIndex);
const group2 = dashIndex === -1 ? [] : numbers.slice(dashIndex + 1); const group2 = dashIndex === -1 ? [] : numbers.slice(dashIndex + 1);
@ -106,7 +106,7 @@ export class SelectionService {
break; break;
} }
case 'QUI': { case 'QNP': {
const dashIndex = numbers.indexOf('-'); const dashIndex = numbers.indexOf('-');
const group1 = dashIndex === -1 ? numbers : numbers.slice(0, dashIndex); const group1 = dashIndex === -1 ? numbers : numbers.slice(0, dashIndex);
const group2 = dashIndex === -1 ? [] : numbers.slice(dashIndex + 1); const group2 = dashIndex === -1 ? [] : numbers.slice(dashIndex + 1);
@ -149,7 +149,7 @@ export class SelectionService {
break; break;
} }
case 'TAN': { case 'TNP': {
const dashIndices = numbers const dashIndices = numbers
.map((n, idx) => (n === '-' ? idx : -1)) .map((n, idx) => (n === '-' ? idx : -1))
.filter(idx => idx !== -1); .filter(idx => idx !== -1);
@ -347,7 +347,7 @@ export class SelectionService {
if (!base.numbers.length || base.value <= 0) return []; if (!base.numbers.length || base.value <= 0) return [];
return ['WIN', 'SHP', 'PLC'].map(label => { return ['WNP', 'SHP', 'PLP'].map(label => {
const newRow: SelectionData = { const newRow: SelectionData = {
...base, ...base,
label, label,

View File

@ -24,14 +24,14 @@ import _ from 'lodash';
export class TouchPadMenuComponent implements OnInit, OnDestroy { export class TouchPadMenuComponent implements OnInit, OnDestroy {
@Input() ticketingActive: boolean = false; @Input() ticketingActive: boolean = false;
public twoGroupLabels = ['FOR', 'QUI']; public twoGroupLabels = ['FRP', 'QNP'];
public multiLegLabels = ['TRE', 'MJP', 'JKP']; public multiLegLabels = ['TRE', 'MJP', 'JKP'];
public threeGroupLabels = ['TAN']; public threeGroupLabels = ['TNP'];
public allowedFieldLabels = ['WIN', 'SHP', 'THP', 'PLC', 'SHW']; public allowedFieldLabels = ['WNP', 'SHP', 'THP', 'PLP', 'SHW'];
labels: string[] = [ labels: string[] = [
'WIN', 'SHP', 'THP', 'PLC', 'SHW', 'FOR', 'WNP', 'SHP', 'THP', 'PLP', 'SHW', 'FRP',
'QUI', 'TAN', 'EXA', 'WSP', 'TRE', 'MJP', 'QNP', 'TNP', 'EXA', 'WSP', 'TRE', 'MJP',
'JKP', 'SJP', '.' 'JKP', 'SJP', '.'
]; ];
@ -53,11 +53,11 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
showLimitPopup: boolean = false; showLimitPopup: boolean = false;
disabledLabels: string[] = ['SHW', 'SJP', '.']; disabledLabels: string[] = ['SHW', 'SJP', '.'];
// TAN logic // TNP logic
tanGroupStage = 0; tanGroupStage = 0;
tanGroups: (number | string)[][] = [[], [], []]; tanGroups: (number | string)[][] = [[], [], []];
// FOR/QUI logic // FRP/QNP logic
isFirstGroupComplete = false; isFirstGroupComplete = false;
firstGroup: (number | string)[] = []; firstGroup: (number | string)[] = [];
secondGroup: (number | string)[] = []; secondGroup: (number | string)[] = [];
@ -257,10 +257,10 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
get showShashEnter(): boolean { get showShashEnter(): boolean {
const label = this.selectedLabel || ''; const label = this.selectedLabel || '';
if (['FOR', 'QUI', 'TAN'].includes(label) && this.isBoxed) { if (['FRP', 'QNP', 'TNP'].includes(label) && this.isBoxed) {
return false; return false;
} }
const specialLabels = ['FOR', 'QUI', 'TAN', 'EXA', 'TRE', 'MJP', 'JKP', '.']; const specialLabels = ['FRP', 'QNP', 'TNP', 'EXA', 'TRE', 'MJP', 'JKP', '.'];
if (this.multiLegLabels.includes(label)) { if (this.multiLegLabels.includes(label)) {
const maxLegs = this.getMaxLegs(this.currentPool || ''); const maxLegs = this.getMaxLegs(this.currentPool || '');
// Hide Shash Enter if on the final leg // Hide Shash Enter if on the final leg
@ -270,7 +270,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
} }
get isShashEnterDisabled(): boolean { get isShashEnterDisabled(): boolean {
if (this.selectedLabel === 'TAN') { if (this.selectedLabel === 'TNP') {
if (this.isBoxed) { if (this.isBoxed) {
return true; return true;
} }
@ -292,15 +292,15 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
get isBoxToggleDisabled(): boolean { get isBoxToggleDisabled(): boolean {
if (!this.selectedLabel) return true; if (!this.selectedLabel) return true;
const disallowedBoxLabels = ['WIN', 'SHP', 'THP', 'PLC', 'WSP', 'SHW', 'TRE', 'MJP', 'JKP']; const disallowedBoxLabels = ['WNP', 'SHP', 'THP', 'PLP', 'WSP', 'SHW', 'TRE', 'MJP', 'JKP'];
if (disallowedBoxLabels.includes(this.selectedLabel)) { if (disallowedBoxLabels.includes(this.selectedLabel)) {
return true; return true;
} }
if (this.selectedNumbers.includes('F')) { if (this.selectedNumbers.includes('F')) {
return true; return true;
} }
// Disable Box toggle for TAN, QUI, FOR after any number is selected // Disable Box toggle for TNP, QNP, FRP after any number is selected
if (['TAN', 'QUI', 'FOR'].includes(this.selectedLabel) && this.selectedNumbers.length > 0) { if (['TNP', 'QNP', 'FRP'].includes(this.selectedLabel) && this.selectedNumbers.length > 0) {
return true; return true;
} }
return false; return false;
@ -308,8 +308,8 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
get isWSPSelection(): boolean { get isWSPSelection(): boolean {
const currentRow = this.selectionService.getCurrentRow(); const currentRow = this.selectionService.getCurrentRow();
return ['WIN', 'SHP', 'PLC'].includes(currentRow.label) || return ['WNP', 'SHP', 'PLP'].includes(currentRow.label) ||
this.selectionService.getSelections().some(sel => ['WIN', 'SHP', 'PLC'].includes(sel.label)); this.selectionService.getSelections().some(sel => ['WNP', 'SHP', 'PLP'].includes(sel.label));
} }
private chunk<T>(array: T[], size: number): T[][] { private chunk<T>(array: T[], size: number): T[][] {
@ -330,19 +330,19 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
if (!this.actualRunners.has(number)) return true; if (!this.actualRunners.has(number)) return true;
// Disable if total amount limit reached // Disable if total amount limit reached
if (this.totalAmountLimitReached) return true; if (this.totalAmountLimitReached) return true;
// Allow all numbers for TAN when boxed, but disable selected numbers // Allow all numbers for TNP when boxed, but disable selected numbers
if (this.selectedLabel === 'TAN' && this.isBoxed) { if (this.selectedLabel === 'TNP' && this.isBoxed) {
return this.selectedNumbers.includes(number); return this.selectedNumbers.includes(number);
} }
// TAN (unboxed): Disable numbers already selected in the current group // TNP (unboxed): Disable numbers already selected in the current group
if (this.selectedLabel === 'TAN') { if (this.selectedLabel === 'TNP') {
return this.tanGroups[this.tanGroupStage].includes(number); return this.tanGroups[this.tanGroupStage].includes(number);
} }
// Multi-leg pools (TRE, MJP, JKP): Disable numbers already selected in the current leg // Multi-leg pools (TRE, MJP, JKP): Disable numbers already selected in the current leg
if (this.multiLegLabels.includes(this.selectedLabel || '')) { if (this.multiLegLabels.includes(this.selectedLabel || '')) {
return this.multiLegGroups[this.multiLegStage].includes(number); return this.multiLegGroups[this.multiLegStage].includes(number);
} }
// Two-group pools (FOR, QUI): Disable numbers already selected in the current group // Two-group pools (FRP, QNP): Disable numbers already selected in the current group
if (this.twoGroupLabels.includes(this.selectedLabel || '')) { if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
if (!this.isFirstGroupComplete) { if (!this.isFirstGroupComplete) {
return this.firstGroup.includes(number); return this.firstGroup.includes(number);
@ -350,7 +350,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
return this.secondGroup.includes(number); return this.secondGroup.includes(number);
} }
} }
// Default case for WIN, SHP, THP, PLC, etc.: Disable if already selected // Default case for WNP, SHP, THP, PLP, etc.: Disable if already selected
return this.selectedNumbers.includes(number); return this.selectedNumbers.includes(number);
} }
@ -402,7 +402,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
//----------------------------------ADDED THIS ----------------------------------------------------- //----------------------------------ADDED THIS -----------------------------------------------------
if (label === 'WSP') { if (label === 'WSP') {
const wspLabels = ['WIN', 'SHP', 'PLC']; const wspLabels = ['WNP', 'SHP', 'PLP'];
this.wspTicketStage = 0; this.wspTicketStage = 0;
this.selectionService.finalizeCurrentRow(); this.selectionService.finalizeCurrentRow();
const currentSelections = this.selectionService.getSelections(); const currentSelections = this.selectionService.getSelections();
@ -429,7 +429,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
// Reset group states // Reset group states
this.tanGroupStage = 0; this.tanGroupStage = 0;
this.tanGroups = [[], [], []]; this.tanGroups = [[], [], []];
// Reset FOR/QUI // Reset FRP/QNP
this.isFirstGroupComplete = false; this.isFirstGroupComplete = false;
this.firstGroup = []; this.firstGroup = [];
this.secondGroup = []; this.secondGroup = [];
@ -466,8 +466,8 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
selectNumber(number: number) { selectNumber(number: number) {
if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return; if (!this.selectedLabel || this.totalAmountLimitReached || !this.actualRunners.has(number)) return;
// TAN boxed // TNP boxed
if (this.selectedLabel === 'TAN' && this.isBoxed) { if (this.selectedLabel === 'TNP' && this.isBoxed) {
if (!this.selectedNumbers.includes(number)) { if (!this.selectedNumbers.includes(number)) {
const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[]; const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[];
const allBoxed = [...currentNumbers, number]; const allBoxed = [...currentNumbers, number];
@ -482,14 +482,14 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
this.selectionService.updatePartial({ this.selectionService.updatePartial({
numbers: [...this.selectedNumbers], numbers: [...this.selectedNumbers],
isBoxed: true, isBoxed: true,
label: 'TAN' label: 'TNP'
}); });
} }
return; return;
} }
// TAN unboxed // TNP unboxed
if (this.selectedLabel === 'TAN') { if (this.selectedLabel === 'TNP') {
if (!this.tanGroups[this.tanGroupStage].includes(number)) { if (!this.tanGroups[this.tanGroupStage].includes(number)) {
this.tanGroups[this.tanGroupStage].push(number); this.tanGroups[this.tanGroupStage].push(number);
const combined: (number | string)[] = [...this.tanGroups[0]]; const combined: (number | string)[] = [...this.tanGroups[0]];
@ -511,7 +511,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy {
} }
// FOR/QUI logic // FRP/QNP logic
if (this.twoGroupLabels.includes(this.selectedLabel || '')) { if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
console.log('Selected label:', this.selectedLabel); console.log('Selected label:', this.selectedLabel);
console.log('Current number clicked:', number); console.log('Current number clicked:', number);
@ -548,10 +548,10 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
if (!this.selectedNumbers.includes(number)) { if (!this.selectedNumbers.includes(number)) {
this.selectedNumbers.push(number); this.selectedNumbers.push(number);
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] }); this.selectionService.updatePartial({ numbers: [...this.selectedNumbers] });
// ✅ Special logic: If WSP, mirror number to WIN, SHP, and THP // ✅ Special logic: If WSP, mirror number to WNP, SHP, and THP
if (this.selectedLabel === 'WSP') { if (this.selectedLabel === 'WSP') {
const labelsToUpdate = ['WIN', 'SHP', 'PLC']; const labelsToUpdate = ['WNP', 'SHP', 'PLP'];
const selections = this.selectionService.getSelections(); const selections = this.selectionService.getSelections();
const updated = selections.map(sel => { const updated = selections.map(sel => {
if (labelsToUpdate.includes(sel.label)) { if (labelsToUpdate.includes(sel.label)) {
@ -583,10 +583,10 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
createVirtualRowsFromWSP(): SelectionData[] { createVirtualRowsFromWSP(): SelectionData[] {
const base = this.selectionService.getCurrentRow(); const base = this.selectionService.getCurrentRow();
if (!base.numbers.length || base.value <= 0 || ['WIN', 'SHP', 'PLC'].includes(base.label)) { if (!base.numbers.length || base.value <= 0 || ['WNP', 'SHP', 'PLP'].includes(base.label)) {
return []; return [];
} }
return ['WIN', 'SHP', 'PLC'].map(label => { return ['WNP', 'SHP', 'PLP'].map(label => {
const newRow: SelectionData = { const newRow: SelectionData = {
...base, ...base,
label, label,
@ -602,7 +602,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
return 0; return 0;
} }
let combinations = 1; let combinations = 1;
if (['TAN', 'FOR', 'QUI'].includes(row.label)) { if (['TNP', 'FRP', 'QNP'].includes(row.label)) {
combinations = row.numbers.length * (row.numbers.length - 1); combinations = row.numbers.length * (row.numbers.length - 1);
} else if (['TRE', 'JKP', 'MJP'].includes(row.label)) { } else if (['TRE', 'JKP', 'MJP'].includes(row.label)) {
const legs = row.numbers.join('').split('/').map(leg => leg.split(',').length); const legs = row.numbers.join('').split('/').map(leg => leg.split(',').length);
@ -617,7 +617,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
clearWSPSelection() { clearWSPSelection() {
if (this.selectedLabel !== 'WSP') return; if (this.selectedLabel !== 'WSP') return;
const labels = ['WIN', 'SHP', 'PLC']; const labels = ['WNP', 'SHP', 'PLP'];
const targetLabel = labels[this.wspTicketStage]; const targetLabel = labels[this.wspTicketStage];
// Update only the current WSP stage's value to 0 // Update only the current WSP stage's value to 0
const updatedSelections = this.selectionService.getSelections().map(sel => { const updatedSelections = this.selectionService.getSelections().map(sel => {
@ -641,7 +641,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
const value = parseFloat(this.padValue) || 0; const value = parseFloat(this.padValue) || 0;
if (this.selectedLabel === 'WSP') { if (this.selectedLabel === 'WSP') {
const labels = ['WIN', 'SHP', 'PLC']; const labels = ['WNP', 'SHP', 'PLP'];
const targetLabel = labels[this.wspTicketStage]; const targetLabel = labels[this.wspTicketStage];
const selections = this.selectionService.getSelections(); const selections = this.selectionService.getSelections();
// Find the current WSP row to ensure numbers are synchronized // Find the current WSP row to ensure numbers are synchronized
@ -660,7 +660,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
}); });
this.selectionService.setSelections(updatedSelections); this.selectionService.setSelections(updatedSelections);
// Only increment stage if not at the last stage (PLC) // Only increment stage if not at the last stage (PLP)
if (this.wspTicketStage < 2) { if (this.wspTicketStage < 2) {
this.wspTicketStage++; this.wspTicketStage++;
@ -678,9 +678,9 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
} }
onShashEnter() { onShashEnter() {
if (this.selectedLabel === 'TAN' && this.isBoxed) return; if (this.selectedLabel === 'TNP' && this.isBoxed) return;
if (this.selectedLabel === 'TAN') { if (this.selectedLabel === 'TNP') {
if (this.tanGroupStage < 2 && this.tanGroups[this.tanGroupStage].length > 0) { if (this.tanGroupStage < 2 && this.tanGroups[this.tanGroupStage].length > 0) {
this.tanGroupStage++; this.tanGroupStage++;
const combined: (number | string)[] = [...this.tanGroups[0]]; const combined: (number | string)[] = [...this.tanGroups[0]];
@ -746,7 +746,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
const value = parseFloat(this.padValue) || 0; const value = parseFloat(this.padValue) || 0;
if (this.selectedLabel === 'WSP') { if (this.selectedLabel === 'WSP') {
const labels = ['WIN', 'SHP', 'PLC']; const labels = ['WNP', 'SHP', 'PLP'];
const targetLabel = labels[this.wspTicketStage]; const targetLabel = labels[this.wspTicketStage];
const updatedSelections = this.selectionService.getSelections().map(sel => { const updatedSelections = this.selectionService.getSelections().map(sel => {
if (sel.label === targetLabel && JSON.stringify(sel.numbers) === JSON.stringify(this.selectedNumbers)) { if (sel.label === targetLabel && JSON.stringify(sel.numbers) === JSON.stringify(this.selectedNumbers)) {
@ -884,8 +884,8 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
const selections = this.selectionService.getSelections(); const selections = this.selectionService.getSelections();
const currentRow = this.selectionService.getCurrentRow(); const currentRow = this.selectionService.getCurrentRow();
if (this.selectedLabel === 'WSP') { if (this.selectedLabel === 'WSP') {
// For WSP, require all three rows (WIN, SHP, PLC) to have valid numbers and values >= 1 // For WSP, require all three rows (WNP, SHP, PLP) to have valid numbers and values >= 1
const wspLabels = ['WIN', 'SHP', 'PLC']; const wspLabels = ['WNP', 'SHP', 'PLP'];
const wspSelections = selections.filter(sel => wspLabels.includes(sel.label)); const wspSelections = selections.filter(sel => wspLabels.includes(sel.label));
const allWSPRowsValid = wspSelections.length === 3 && wspSelections.every(row => const allWSPRowsValid = wspSelections.length === 3 && wspSelections.every(row =>
row.label && row.numbers && row.numbers.length > 0 && row.value >= 1 && row.total > 0 row.label && row.numbers && row.numbers.length > 0 && row.value >= 1 && row.total > 0
@ -933,7 +933,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
if (this.selectedLabel === 'WSP') { if (this.selectedLabel === 'WSP') {
const virtualRows = this.createVirtualRowsFromWSP(); const virtualRows = this.createVirtualRowsFromWSP();
const currentSelections = this.selectionService.getSelections(); const currentSelections = this.selectionService.getSelections();
const nonWSPSelections = currentSelections.filter(sel => !['WIN', 'SHP', 'PLC'].includes(sel.label)); const nonWSPSelections = currentSelections.filter(sel => !['WNP', 'SHP', 'PLP'].includes(sel.label));
this.selectionService.setSelections([...nonWSPSelections, ...virtualRows]); this.selectionService.setSelections([...nonWSPSelections, ...virtualRows]);
} }
@ -972,7 +972,7 @@ if (this.twoGroupLabels.includes(this.selectedLabel || '')) {
//-------------------PRINT LOGIC---------------------------------------- //-------------------PRINT LOGIC----------------------------------------
printTicket() { async printTicket() {
const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + sel.total, 0); const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + sel.total, 0);
if (selectionsTotal + this.currentTotal > 5000) { if (selectionsTotal + this.currentTotal > 5000) {
this.showLimitPopup = true; this.showLimitPopup = true;
@ -987,7 +987,7 @@ printTicket() {
const currentRow = this.selectionService.getCurrentRow(); const currentRow = this.selectionService.getCurrentRow();
if (this.selectedLabel === 'WSP') { if (this.selectedLabel === 'WSP') {
selections = selections.filter(sel => ['WIN', 'SHP', 'PLC'].includes(sel.label)); selections = selections.filter(sel => ['WNP', 'SHP', 'PLP'].includes(sel.label));
} }
let allRows = [...selections]; let allRows = [...selections];
@ -1003,7 +1003,7 @@ printTicket() {
if (!currentRow.total) { if (!currentRow.total) {
let combinations = 1; let combinations = 1;
if (['TAN', 'FOR', 'QUI'].includes(currentRow.label)) { if (['TNP', 'FRP', 'QNP'].includes(currentRow.label)) {
combinations = currentRow.numbers.length * (currentRow.numbers.length - 1); combinations = currentRow.numbers.length * (currentRow.numbers.length - 1);
} else if (['TRE', 'JKP', 'MJP'].includes(currentRow.label)) { } else if (['TRE', 'JKP', 'MJP'].includes(currentRow.label)) {
combinations = 1; // or your specific logic combinations = 1; // or your specific logic
@ -1026,7 +1026,7 @@ printTicket() {
const totalAmount = allRows.reduce((sum, row) => sum + (row.total || 0), 0); const totalAmount = allRows.reduce((sum, row) => sum + (row.total || 0), 0);
const now = new Date(); const now = new Date();
const venue = 'MYS'; const venue = this.structuredRaceCard?.venue || '';
const day = String(now.getDate()).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0');
const year = String(now.getFullYear()).slice(-2); const year = String(now.getFullYear()).slice(-2);
@ -1156,8 +1156,8 @@ displayNumbers = displayNumbers.flatMap((n) => {
let numbersStr = ''; let numbersStr = '';
// 🎯 FOR, QUI, TAN logic with box check // 🎯 FRP, QNP, TNP logic with box check
if (['FOR', 'QUI'].includes(row.label)) { if (['FRP', 'QNP'].includes(row.label)) {
// const actualNumbers = displayNumbers.filter(n => n !== '#' && n !== '-').join(','); // const actualNumbers = displayNumbers.filter(n => n !== '#' && n !== '-').join(',');
const actualNumbers = displayNumbers.filter(n => n !== '#').join(','); const actualNumbers = displayNumbers.filter(n => n !== '#').join(',');
if (row.numbers.includes('#') || row.isBoxed) { // ✅ box condition if (row.numbers.includes('#') || row.isBoxed) { // ✅ box condition
@ -1179,7 +1179,7 @@ displayNumbers = displayNumbers.flatMap((n) => {
return `${label}${numbers} ${value} ${total}`; return `${label}${numbers} ${value} ${total}`;
}).join('\n'); }).join('\n');
//------------------------------------WIN LABELS ENDS HERE -------------------------------- //------------------------------------WNP LABELS ENDS HERE --------------------------------
// ✅ Print preview // ✅ Print preview
const printData = { const printData = {
@ -1205,6 +1205,213 @@ displayNumbers = displayNumbers.flatMap((n) => {
console.log(`Date/Time : ${now.toLocaleString()}`); console.log(`Date/Time : ${now.toLocaleString()}`);
console.log('-----------------------------'); console.log('-----------------------------');
// --------------------------------------------CONSOLE LOGS EDITTED FORMATTED ----------------------------------
// ------------------- Simulated console-ticket printing (REPLACE THIS BLOCK) -------------------
console.log('--- Simulated Ticket Print ---');
console.log(`Ticket ID : ${printData.ticketId}`);
console.log(`Barcode ID : ${printData.barcodeId}`);
console.log(`|||||| ||| | ||||| |||| |`); // Dummy barcode
console.log(`WIN Labels :`);
// helper to pad each horse number in a leg (handles " / " multi-leg separators)
function formatNumbers(numStr: string) {
const legs = numStr.split(/\s*\/\s*/).map(leg => {
const parts = leg.split(',').map(p => p.trim()).filter(Boolean);
return parts.map(p => {
const numeric = p.match(/^\d+$/);
return numeric ? p.padStart(2, '0') : p;
}).join(',');
});
return legs.join('/');
}
// helper to pad ticket count to 3 digits (001..999)
function padTicketCountToThree(n: number) {
const num = Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
return String(num).padStart(3, '0');
}
// Build compact entries "LABEL numbers*NNN"
const formattedEntries: string[] = [];
printData.winLabels.split('\n').forEach(line => {
const m = line.match(/^(\S+)\s+(.+?)\s+\*(\d+)\b/);
if (m) {
const label = m[1];
const rawNums = m[2].trim();
const value = Number(m[3]) || 0;
const formattedNums = formatNumbers(rawNums);
const valueStr = `*${padTicketCountToThree(value)}`;
formattedEntries.push(`${label} ${formattedNums}${valueStr}`);
} else {
if (line.trim()) formattedEntries.push(line.trim());
}
});
function compactifyEntry(entry: string) {
const m = entry.match(/^(\S+)\s+(.+?)(\*\d{1,})?$/);
if (!m) return entry;
const label = m[1];
let numsPart = (m[2] || '').trim();
const starPart = m[3] || '';
// Normalize whitespace
numsPart = numsPart.replace(/\s+/g, ' ').trim();
// Build groups by treating '-' token as a group separator
const rawTokens = numsPart.split(',').map(s => s.trim());
const groups: string[][] = [];
let currentGroup: string[] = [];
for (const tok of rawTokens) {
if (!tok || tok === '#') {
// skip stray empty / '#' tokens
continue;
}
if (tok === '-') {
// push current group (even if empty) and start new group
groups.push(currentGroup);
currentGroup = [];
continue;
}
currentGroup.push(tok);
}
// push last group
groups.push(currentGroup);
// helper to normalize token (zero-pad numeric)
const normalizeToken = (tok: string) => {
const num = tok.match(/^\d+$/);
return num ? tok.padStart(2, '0') : tok;
};
// Filter out groups that became empty due to stray tokens, but preserve intentional empties if user had them.
const filteredGroups = groups
.map(g => g.filter(t => t && t !== '-' && t !== '#'))
.filter((g, idx) => !(g.length === 0 && groups.length === 1)); // keep single empty group only if it was the only group
// If there are multiple groups (from '-' markers), format them joined by hyphen
if (filteredGroups.length > 1) {
const groupStrs = filteredGroups.map(g => g.map(normalizeToken).join(','));
return `${label} ${groupStrs.join('-')}${starPart}`;
}
// No explicit '-' groups (single group only). fallback to duplicated-halves detection
const singleTokens = filteredGroups[0] || [];
if (singleTokens.length >= 2 && singleTokens.length % 2 === 0) {
const half = singleTokens.length / 2;
const firstHalf = singleTokens.slice(0, half).map(normalizeToken).join(',');
const secondHalf = singleTokens.slice(half).map(normalizeToken).join(',');
if (firstHalf === secondHalf) {
return `${label} ${firstHalf}-${secondHalf}${starPart}`;
}
}
// Final fallback: normalized comma-joined tokens
const normalized = singleTokens.map(normalizeToken).join(',');
return `${label} ${normalized}${starPart}`;
}
// Apply compactify to each entry and join into one string
let formattedWinLabels = formattedEntries.map(compactifyEntry).join('');
// Fallback if nothing parsed
if (!formattedWinLabels) {
formattedWinLabels = printData.winLabels || '(no win labels)';
}
// Show formatted result and totals
const formattedTotal = `${printData.totalAmount}`;
console.log(formattedWinLabels);
console.log(formattedTotal);
console.log(`GST Number : ${printData.gstNumber}`);
console.log(`Date/Time : ${now.toLocaleString()}`);
console.log('-----------------------------');
// ---------------------------------- CONSOLE LOG ENDS HERE ---------------------------------------------------------
//----------------------------------------SENDIND DATA TO BACKEND ----------------------------------------------------
// Build the commit payload as before (btId, raceVenue, raceDt, raceNum, betInfo, nHorseBits, tktVal)
const ticketParts = (printData.ticketId || '').split('/');
const raceVenue = ticketParts[0] || 'MYS';
const dateStr = ticketParts[1] || ''; // "20250907"
const raceDt = dateStr && dateStr.length === 8
? `${dateStr.slice(0,4)}/${dateStr.slice(4,6)}/${dateStr.slice(6,8)}`
: (new Date()).toISOString().slice(0,10).replace(/-/g,'/');
const raceNumRaw = ticketParts[2] || '1';
const raceNum = String(Number(raceNumRaw)).padStart(2,'0');
const btId_bc = (printData.barcodeId || '0000').toString().slice(0,4);
const betInfo = formattedWinLabels || printData.winLabels || '';
const nHorseBits = [0,0,0,0,0,0,0,0];
const tktVal = Number(printData.totalAmount) || 0;
const commitPayload = {
btMake: "I",
btId: btId_bc,
raceVenue: raceVenue,
raceDt: raceDt,
raceNum: raceNum,
betInfo: betInfo,
nHorseBits: nHorseBits,
moneyTyp: "C",
tktVal: tktVal,
repeatFlag: "N"
};
console.log('➡️ Sending commit to /isr/commit:', commitPayload);
let commitJson = null;
try {
const commitResp = await fetch('http://localhost:8084/isr/commit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(commitPayload),
// optionally set a timeout by using AbortController if you need it
});
// attempt to parse JSON safely
const txt = await commitResp.text();
try {
commitJson = txt ? JSON.parse(txt) : null;
} catch (e) {
console.warn('⚠️ /isr/commit returned non-JSON response:', txt);
commitJson = null;
}
// If HTTP status not OK, treat as failure
if (!commitResp.ok) {
console.error('❌ /isr/commit HTTP error', commitResp.status, commitJson ?? txt);
return; // stop — don't print
}
} catch (err) {
console.error('❌ Network/error while calling /isr/commit:', err);
return; // stop — don't print
}
// Check API-level success flag before proceeding to print
// Adjust this line if your API uses a different success indicator.
if (!(commitJson && commitJson.success === true)) {
console.error('❌ /isr/commit failed or returned success=false:', commitJson);
// Optionally surface error message from server
if (commitJson && commitJson.message) {
console.error('Server message:', commitJson.message);
}
return; // do not proceed to printing
}
// If we reach here, commit succeeded
console.log('✅ /isr/commit success:', commitJson);
//---------------------------------------------- BACK END ENDS HERE -----------------------------------------------------
// 🖨️ Send to printer API // 🖨️ Send to printer API
const payload = { const payload = {
type: 'ticket', type: 'ticket',
@ -1337,7 +1544,7 @@ try {
if (this.totalAmountLimitReached) return; if (this.totalAmountLimitReached) return;
this.isBoxed = !this.isBoxed; this.isBoxed = !this.isBoxed;
const value = parseFloat(this.padValue) || 0; const value = parseFloat(this.padValue) || 0;
if (this.selectedLabel === 'TAN' && this.isBoxed) { if (this.selectedLabel === 'TNP' && this.isBoxed) {
this.tanGroupStage = 0; this.tanGroupStage = 0;
this.tanGroups = [[], [], []]; this.tanGroups = [[], [], []];
this.selectedNumbers = []; this.selectedNumbers = [];
@ -1360,7 +1567,7 @@ try {
return; return;
} }
if (this.selectedLabel === 'TAN' && this.isBoxed) { if (this.selectedLabel === 'TNP' && this.isBoxed) {
const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[]; const currentNumbers = this.selectedNumbers.filter(n => typeof n === 'number') as number[];
if (currentNumbers.length > 0) { if (currentNumbers.length > 0) {
currentNumbers.pop(); currentNumbers.pop();
@ -1372,12 +1579,12 @@ try {
if (group2.length) combined.push('-', ...group2); if (group2.length) combined.push('-', ...group2);
if (group3.length) combined.push('-', ...group3); if (group3.length) combined.push('-', ...group3);
this.selectedNumbers = combined; this.selectedNumbers = combined;
this.selectionService.updatePartial({ numbers: [...this.selectedNumbers], isBoxed: true, label: 'TAN' }); this.selectionService.updatePartial({ numbers: [...this.selectedNumbers], isBoxed: true, label: 'TNP' });
} }
return; return;
} }
if (this.selectedLabel === 'TAN') { if (this.selectedLabel === 'TNP') {
const currentGroup = this.tanGroups[this.tanGroupStage]; const currentGroup = this.tanGroups[this.tanGroupStage];
if (currentGroup.length > 0) { if (currentGroup.length > 0) {
currentGroup.pop(); currentGroup.pop();
@ -1459,7 +1666,7 @@ try {
private handleFieldForSpecialLabels() { private handleFieldForSpecialLabels() {
if (!this.selectedLabel) return; if (!this.selectedLabel) return;
if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') { if (this.selectedLabel === 'FRP' || this.selectedLabel === 'QNP') {
if (!this.isFirstGroupComplete) { if (!this.isFirstGroupComplete) {
this.firstGroup = ['F']; this.firstGroup = ['F'];
this.selectedNumbers = ['F']; this.selectedNumbers = ['F'];
@ -1469,7 +1676,7 @@ try {
this.selectedNumbers = [...this.firstGroup, '-', 'F']; this.selectedNumbers = [...this.firstGroup, '-', 'F'];
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
} }
} else if (this.selectedLabel === 'TAN') { } else if (this.selectedLabel === 'TNP') {
if (this.isBoxed) { if (this.isBoxed) {
this.selectedNumbers = ['F']; this.selectedNumbers = ['F'];
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true }); this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true });
@ -1500,13 +1707,13 @@ try {
canUseField(): boolean { canUseField(): boolean {
if (this.totalAmountLimitReached) return false; if (this.totalAmountLimitReached) return false;
if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') { if (this.selectedLabel === 'FRP' || this.selectedLabel === 'QNP') {
if (!this.isFirstGroupComplete && this.firstGroup.length === 0) return true; if (!this.isFirstGroupComplete && this.firstGroup.length === 0) return true;
if (this.isFirstGroupComplete && this.secondGroup.length === 0) return true; if (this.isFirstGroupComplete && this.secondGroup.length === 0) return true;
return false; return false;
} }
if (this.selectedLabel === 'TAN' || this.multiLegLabels.includes(this.selectedLabel || '')) { if (this.selectedLabel === 'TNP' || this.multiLegLabels.includes(this.selectedLabel || '')) {
if (this.selectedLabel === 'TAN' && this.tanGroups[this.tanGroupStage].length === 0) return true; if (this.selectedLabel === 'TNP' && this.tanGroups[this.tanGroupStage].length === 0) return true;
if (this.multiLegLabels.includes(this.selectedLabel || '') && this.multiLegGroups[this.multiLegStage].length === 0) return true; if (this.multiLegLabels.includes(this.selectedLabel || '') && this.multiLegGroups[this.multiLegStage].length === 0) return true;
return false; return false;
} }
@ -1515,7 +1722,7 @@ try {
openFieldModal() { openFieldModal() {
if (this.totalAmountLimitReached) return; if (this.totalAmountLimitReached) return;
if (['FOR', 'QUI', 'TAN'].includes(this.selectedLabel || '') || this.multiLegLabels.includes(this.selectedLabel || '')) { if (['FRP', 'QNP', 'TNP'].includes(this.selectedLabel || '') || this.multiLegLabels.includes(this.selectedLabel || '')) {
this.handleFieldForSpecialLabels(); this.handleFieldForSpecialLabels();
} else { } else {
this.fieldModalOpen = true; this.fieldModalOpen = true;
@ -1545,7 +1752,7 @@ try {
confirmFieldEntry() { confirmFieldEntry() {
if (!this.fieldFEntered || !this.selectedLabel) return; if (!this.fieldFEntered || !this.selectedLabel) return;
if (this.selectedLabel === 'FOR' || this.selectedLabel === 'QUI') { if (this.selectedLabel === 'FRP' || this.selectedLabel === 'QNP') {
if (!this.isFirstGroupComplete) { if (!this.isFirstGroupComplete) {
this.firstGroup = ['F']; this.firstGroup = ['F'];
this.selectedNumbers = ['F']; this.selectedNumbers = ['F'];
@ -1554,7 +1761,7 @@ try {
this.selectedNumbers = [...this.firstGroup, '-', 'F']; this.selectedNumbers = [...this.firstGroup, '-', 'F'];
} }
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false }); this.selectionService.updatePartial({ label: this.selectedLabel, numbers: [...this.selectedNumbers], isBoxed: false });
} else if (this.selectedLabel === 'TAN') { } else if (this.selectedLabel === 'TNP') {
if (this.isBoxed) { if (this.isBoxed) {
this.selectedNumbers = ['F']; this.selectedNumbers = ['F'];
this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true }); this.selectionService.updatePartial({ label: this.selectedLabel, numbers: ['F'], isBoxed: true });
@ -1579,8 +1786,8 @@ try {
openPoolReplaceModal() { openPoolReplaceModal() {
if (this.totalAmountLimitReached) return; if (this.totalAmountLimitReached) return;
const groupA = ['WIN', 'SHP', 'THP', 'PLC', 'SHW']; const groupA = ['WNP', 'SHP', 'THP', 'PLP', 'SHW'];
const groupB = ['FOR', 'QUI', 'TAN']; const groupB = ['FRP', 'QNP', 'TNP'];
if (groupA.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupA; if (groupA.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupA;
else if (groupB.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupB; else if (groupB.includes(this.selectedLabel || '')) this.poolReplaceOptions = groupB;
else this.poolReplaceOptions = []; else this.poolReplaceOptions = [];