feat : stop message sync with 2nd screen also

This commit is contained in:
karthik 2025-09-18 12:27:47 +05:30
parent 7ebce2dbcd
commit 1cfe2fd970
9 changed files with 153 additions and 106 deletions

View File

@ -1,4 +1,3 @@
// main.js
const { app, BrowserWindow, screen, ipcMain } = require('electron'); const { app, BrowserWindow, screen, ipcMain } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
@ -23,7 +22,7 @@ function createWindows() {
}, },
}); });
mainWindow.loadURL('http://10.74.231.124:4200/login'); mainWindow.loadURL('http://10.150.40.124:4200/login');
screenWindow = new BrowserWindow({ screenWindow = new BrowserWindow({
x: secondary.bounds.x, x: secondary.bounds.x,
@ -38,7 +37,7 @@ function createWindows() {
}, },
}); });
screenWindow.loadURL('http://10.74.231.124:4200/shared-display'); screenWindow.loadURL('http://10.150.40.124:4200/shared-display');
// Handle opening second screen and send initial data // Handle opening second screen and send initial data
ipcMain.on('open-second-screen', () => { ipcMain.on('open-second-screen', () => {
@ -58,6 +57,13 @@ function createWindows() {
} }
}); });
// Add this new handler
ipcMain.on('sync-stop-message', (event, message) => {
if (screenWindow && screenWindow.webContents) {
screenWindow.webContents.send('update-stop-message', message);
}
});
// Handle BTID request // Handle BTID request
ipcMain.handle('get-btid', () => { ipcMain.handle('get-btid', () => {
try { try {

View File

@ -1,12 +1,12 @@
// preload.js
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
openSecondScreen: () => ipcRenderer.send('open-second-screen'), openSecondScreen: () => ipcRenderer.send('open-second-screen'),
closeSecondScreen: () => ipcRenderer.send('close-second-screen'), closeSecondScreen: () => ipcRenderer.send('close-second-screen'),
getBtid: () => ipcRenderer.invoke('get-btid'), getBtid: () => ipcRenderer.invoke('get-btid'),
// New: Send data to main process
syncSharedData: (data) => ipcRenderer.send('sync-shared-data', data), syncSharedData: (data) => ipcRenderer.send('sync-shared-data', data),
// New: Receive data in second window
onUpdateSharedData: (callback) => ipcRenderer.on('update-shared-data', (event, data) => callback(data)), onUpdateSharedData: (callback) => ipcRenderer.on('update-shared-data', (event, data) => callback(data)),
// Add these new lines
syncStopMessage: (message) => ipcRenderer.send('sync-stop-message', message),
onUpdateStopMessage: (callback) => ipcRenderer.on('update-stop-message', (event, message) => callback(message)),
}); });

View File

@ -668,3 +668,28 @@
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
/* Style for the scrolling stop message */
.stop-message {
background-color: red;
color: white;
font-weight: bold;
padding: 2px 5px;
margin: 0 10px;
border-radius: 4px;
display: inline-block;
max-width: 50%;
overflow: hidden;
}
.stop-message marquee {
font-size: 14px;
line-height: 20px;
}
/* Existing styles for other elements */
.text-light {
color: white !important;
}
/* Add other existing styles as needed */

View File

@ -3,27 +3,28 @@
<div class="text-light small ps-3"> <div class="text-light small ps-3">
<span class="date-time">{{ dateTime }}</span> <span class="date-time">{{ dateTime }}</span>
</div> </div>
<div class="flex-grow-1 text-center">
<div *ngIf="stopMessage" class="stop-message">
<marquee behavior="scroll" direction="left" scrollamount="5">{{ stopMessage }}</marquee>
</div>
</div>
<div <div
class="d-flex align-items-center text-light small justify-content-end flex-grow-1" class="d-flex align-items-center text-light small justify-content-end flex-grow-1"
> >
<span class="btno-label btno-space"> <span class="btno-label btno-space">
<b>Operator:</b> {{ userName }} <b>Operator:</b> {{ userName }}
</span> </span>
<span class="btno-label me-3"> <span class="btno-label me-3">
<b>B.T.No: </b> {{ btid }} <b>B.T.No: </b> {{ btid }}
</span> </span>
<div <div
class="status_box text-center" class="status_box text-center"
[ngClass]="liveStatusOk ? 'live-green' : 'live-red'" [ngClass]="liveStatusOk ? 'live-green' : 'live-red'"
> >
@if( liveStatusOk){ @if (liveStatusOk) {
<span>LIVE</span> <span>LIVE</span>
} @else { } @else {
<span>OFF-LINE</span> <span>OFF-LINE</span>
} }
</div> </div>
</div> </div>
@ -106,7 +107,6 @@
> >
Race No. | {{ selectedRace }} Race No. | {{ selectedRace }}
</button> </button>
<div class="d-flex flex-column text-end"> <div class="d-flex flex-column text-end">
<div <div
class="nav-button w-100 text-end pe-3 py-2 border-top" class="nav-button w-100 text-end pe-3 py-2 border-top"
@ -217,18 +217,10 @@
</div> </div>
<!-- View Log Modal --> <!-- View Log Modal -->
<!-- <div class="viewlog-modal-overlay" *ngIf="showLogModal" (click)="closeModals()">
<div class="viewlog-modal-box" (click)="$event.stopPropagation()">
<h5>VIEW-LOG</h5>
<button class="cancel-btn" (click)="closeModals()">CANCEL</button>
</div>
</div> -->
<div class="viewlog-modal-overlay" *ngIf="showLogModal" (click)="closeModals()"> <div class="viewlog-modal-overlay" *ngIf="showLogModal" (click)="closeModals()">
<div class="viewlog-modal-box" (click)="$event.stopPropagation()"> <div class="viewlog-modal-box" (click)="$event.stopPropagation()">
<!-- Modal Header --> <!-- Modal Header -->
<h3 class="modal-title">VIEW-LOG</h3> <h3 class="modal-title">VIEW-LOG</h3>
<!-- Scrollable Table Area --> <!-- Scrollable Table Area -->
<div class="table-container"> <div class="table-container">
<table class="view-log-table"> <table class="view-log-table">
@ -240,7 +232,7 @@
<th>Tickets</th> <th>Tickets</th>
<th>Count</th> <th>Count</th>
<th>Amount</th> <th>Amount</th>
<th>Barcode</th> <!-- ✅ New Column --> <th>Barcode</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -256,7 +248,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- CANCEL Button at Bottom --> <!-- CANCEL Button at Bottom -->
<div class="modal-footer"> <div class="modal-footer">
<button class="cancel-btn" (click)="closeModals()">CANCEL</button> <button class="cancel-btn" (click)="closeModals()">CANCEL</button>
@ -264,48 +255,6 @@
</div> </div>
</div> </div>
<!-- <div
class="viewlog-modal-overlay"
*ngIf="showLogModal"
(click)="closeModals()"
>
<div class="viewlog-modal-box" (click)="$event.stopPropagation()">
<h5 class="modal-title text-center">VIEW-LOG</h5>
<div class="modal-body" style="max-height: 400px; overflow-y: auto;">
<ng-container *ngIf="formattedTicketLogs.length > 0; else noLogs">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="text-align: left;">
<th>Pool</th>
<th>Numbers</th>
<th>×Count</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let log of formattedTicketLogs">
<td>{{ log.label }}</td>
<td>{{ log.numbers.join(',') }}</td>
<td>*{{ log.count }}</td>
<td>₹ {{ log.amount }}</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-template #noLogs>
<p>No tickets found.</p>
</ng-template>
</div>
<div class="modal-footer text-center mt-3">
<button class="cancel-btn" (click)="closeModals()">CANCEL</button>
</div>
</div>
</div> -->
<!-- Venue Modal --> <!-- Venue Modal -->
<div class="modal-overlay" *ngIf="showVenueModal" (click)="closeModals()"> <div class="modal-overlay" *ngIf="showVenueModal" (click)="closeModals()">
<div class="modal-box" (click)="$event.stopPropagation()"> <div class="modal-box" (click)="$event.stopPropagation()">

View File

@ -44,7 +44,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
selectedRaceId: number = 0; selectedRaceId: number = 0;
enabledHorseNumbers: number[] = []; enabledHorseNumbers: number[] = [];
multiLegBaseRaceIdx: number = 0; multiLegBaseRaceIdx = 0;
currentPool: string | null = null; currentPool: string | null = null;
private prevEnabledKey = ''; private prevEnabledKey = '';
@ -75,6 +75,9 @@ export class NavbarComponent implements OnInit, OnDestroy {
private currentDate: string = ''; private currentDate: string = '';
private eventSource: EventSource | undefined; private eventSource: EventSource | undefined;
stopMessage: string = '';
private stopMessageTimeout: number | null = null;
formattedTicketLogs: { formattedTicketLogs: {
pool: string; pool: string;
horses: string; horses: string;
@ -108,7 +111,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
}, 1000); }, 1000);
}); });
// === Load structuredRaceCard from rpinfo if available (prefer structured) === // Load structuredRaceCard from rpinfo if available (prefer structured)
const rpCached = localStorage.getItem('rpinfo'); const rpCached = localStorage.getItem('rpinfo');
if (rpCached) { if (rpCached) {
try { try {
@ -120,7 +123,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.raceCardData = {}; this.raceCardData = {};
} }
} else { } else {
// fallback to older raceCardData key if present (non-structured) // Fallback to older raceCardData key if present (non-structured)
const fallback = localStorage.getItem('raceCardData'); const fallback = localStorage.getItem('raceCardData');
if (fallback) { if (fallback) {
try { try {
@ -155,10 +158,51 @@ export class NavbarComponent implements OnInit, OnDestroy {
const statuses$ = this.stopbetService.getStopbetStatuses(); const statuses$ = this.stopbetService.getStopbetStatuses();
if (statuses$ && typeof statuses$.subscribe === 'function') { if (statuses$ && typeof statuses$.subscribe === 'function') {
this.stopbetSubscription = statuses$.subscribe((statuses: Map<number, string>) => { this.stopbetSubscription = statuses$.subscribe((statuses: Map<number, string>) => {
this.stopbetStatuses.clear(); const newStatuses = new Map(statuses);
statuses.forEach((value, key) => { const newStops: number[] = [];
this.stopbetStatuses.set(key, value);
// Detect newly stopped races
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 the message for 30 seconds
if (newStops.length > 0) {
newStops.sort((a, b) => a - b);
this.stopMessage = newStops.map(r => `Race ${r} Stopped`).join(' - ');
// Sync new 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);
}
}
if (this.stopMessageTimeout) {
clearTimeout(this.stopMessageTimeout);
}
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)); console.log('[NAVBAR] Updated stopbetStatuses:', Object.fromEntries(this.stopbetStatuses));
this.checkAndSwitchIfStopped(); this.checkAndSwitchIfStopped();
this.updateEnabledHorseNumbers(); this.updateEnabledHorseNumbers();
@ -432,7 +476,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
openVenueModal() { openVenueModal() {
// Prefer structuredRaceCard loaded at init (rpinfo). If empty, try legacy key.
if (!this.raceCardData || Object.keys(this.raceCardData).length === 0) { if (!this.raceCardData || Object.keys(this.raceCardData).length === 0) {
const cachedData = localStorage.getItem('raceCardData'); const cachedData = localStorage.getItem('raceCardData');
if (cachedData) { if (cachedData) {
@ -447,7 +490,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
} }
// Use lowercase 'venue' as structuredRaceCard uses .venue
this.selectedVenue = this.raceCardData?.venue ?? this.raceCardData?.Venue ?? 'Select Venue'; this.selectedVenue = this.raceCardData?.venue ?? this.raceCardData?.Venue ?? 'Select Venue';
this.updateEnabledHorseNumbers(); this.updateEnabledHorseNumbers();
this.showVenueModal = true; this.showVenueModal = true;
@ -500,7 +542,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
} }
selectRace(race: number) { selectRace(race: number) {
// Adjust race if the attempted one is stopped
const adjustedRace = this.isOpen(race) ? race : this.getOpenRaceStartingFrom(race); const adjustedRace = this.isOpen(race) ? race : this.getOpenRaceStartingFrom(race);
this.selectedRace = adjustedRace; this.selectedRace = adjustedRace;
@ -513,11 +554,9 @@ export class NavbarComponent implements OnInit, OnDestroy {
value: this.selectedRace, value: this.selectedRace,
}); });
// Use this.raceCardData (structured) rather than re-parsing localStorage
const raceList = this.raceCardData?.raceVenueRaces?.races ?? []; const raceList = this.raceCardData?.raceVenueRaces?.races ?? [];
const selectedRaceEntry = raceList[this.selectedRace - 1] ?? []; const selectedRaceEntry = raceList[this.selectedRace - 1] ?? [];
// Determine runnerCount defensively based on possible shapes
let runnerCount = 12; let runnerCount = 12;
if (Array.isArray(selectedRaceEntry)) { if (Array.isArray(selectedRaceEntry)) {
runnerCount = selectedRaceEntry.length || 12; runnerCount = selectedRaceEntry.length || 12;
@ -573,22 +612,18 @@ export class NavbarComponent implements OnInit, OnDestroy {
displayBarcode = ''; displayBarcode = '';
if (rawLabel) { if (rawLabel) {
// 1⃣ Extract pool (first word)
const parts = rawLabel.split(/\s+/); const parts = rawLabel.split(/\s+/);
pool = parts[0]; pool = parts[0];
// 2⃣ Extract ticket count (*n) & price if exists
const countMatch = rawLabel.match(/\*(\d+)(?:\s+(Rs\s*\d+))?/); const countMatch = rawLabel.match(/\*(\d+)(?:\s+(Rs\s*\d+))?/);
if (countMatch) { if (countMatch) {
ticketCountLabel = `*${countMatch[1]}`; ticketCountLabel = `*${countMatch[1]}`;
price = countMatch[2] || ''; price = countMatch[2] || '';
} }
// 3⃣ Extract horses part (between pool name & ticket count)
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
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()));
} else { } else {
@ -627,7 +662,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 stopbetData = localStorage.getItem('stopbetStatuses');
const printData = { const printData = {
name, name,
@ -675,9 +710,11 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.eventSource.close(); this.eventSource.close();
} catch {} } catch {}
} }
if (this.stopMessageTimeout) {
clearTimeout(this.stopMessageTimeout);
}
} }
// Add trackByHorse for use in *ngFor
trackByHorse(index: number, item: number): number { trackByHorse(index: number, item: number): number {
return item; return item;
} }

View File

@ -98,14 +98,14 @@ div[style*="background-color: black"] .custom-cell {
/* === Footer Always at Bottom === */ /* === Footer Always at Bottom === */
.footer { .footer {
height: 10vh; height: 10vh;
background-color: #1c2c46a8; background-color: #fafbfca2;
color: white; color: rgb(126, 0, 0);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 15%; margin-top: 15%;
font-weight: bold; font-weight: bold;
font-size: 1rem; font-size: 1.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
} }
@ -160,3 +160,21 @@ div[style*="background-color: black"] .custom-cell {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
} }
/* Add these new styles for scrolling */
.live-data-text.scrolling {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
animation: scroll 20s linear infinite; /* Adjust duration for speed */
width: 55%; /* Ensure it takes full width */
}
@keyframes scroll {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}

View File

@ -44,9 +44,10 @@
</div> </div>
</div> </div>
<footer class="footer" [ngStyle]="{ 'background-color': isConnected ? '#d1ffd1' : '#ffcccc' }"> <!-- Updated footer: Conditional message, background, and scrolling class -->
<div class="live-data-text"> <footer class="footer" >
Live Data: {{ message || 'Disconnected' }} <div class="live-data-text" [class.scrolling]="!!stopMessage">
</div> {{ stopMessage ? ('Races Stopped: ' + stopMessage) : (message ? ('Live Data: ' + message) : 'Hello everyone!') }}
</footer> </div>
</footer>
</div> </div>

View File

@ -1,4 +1,3 @@
// shared-table.component.ts
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SelectionData } from '../selection.service/selection.service'; import { SelectionData } from '../selection.service/selection.service';
@ -19,18 +18,28 @@ export class SharedTableComponent implements OnInit {
message = ''; message = '';
isConnected = false; isConnected = false;
// Add this new property
stopMessage: string = '';
constructor(private websocketService: WebsocketService) {} constructor(private websocketService: WebsocketService) {}
ngOnInit() { ngOnInit() {
this.websocketService.message$.subscribe((msg) => { // this.websocketService.message$.subscribe((msg) => {
this.message = msg; // this.message = msg;
console.log('[SHARED TABLE] WebSocket message:', msg); // console.log('[SHARED TABLE] WebSocket message:', msg);
}); // });
this.websocketService.isConnected$.subscribe((status) => { // this.websocketService.isConnected$.subscribe((status) => {
this.isConnected = status; // this.isConnected = status;
console.log('[SHARED TABLE] WebSocket connection status:', status); // console.log('[SHARED TABLE] WebSocket connection status:', status);
}); // });
// Add this: Listen for stop message updates
if (window.electronAPI) {
window.electronAPI.onUpdateStopMessage((message: string) => {
this.stopMessage = message;
console.log('[SHARED TABLE] Received stop message:', message);
});
}
} }
} }

View File

@ -1,10 +1,12 @@
// src/electron.d.ts
interface ElectronAPI { interface ElectronAPI {
openSecondScreen: () => void; openSecondScreen: () => void;
closeSecondScreen: () => void; closeSecondScreen: () => void;
getBtid: () => Promise<string | null>; getBtid: () => Promise<string | null>;
syncSharedData: (data: SharedData) => void; syncSharedData: (data: SharedData) => void;
onUpdateSharedData: (callback: (data: SharedData) => void) => void; onUpdateSharedData: (callback: (data: SharedData) => void) => void;
// Add these new lines
syncStopMessage: (message: string) => void;
onUpdateStopMessage: (callback: (message: string) => void) => void;
} }
interface SharedData { interface SharedData {