diff --git a/btc-UI/src/app/components/selection.service/label-restriction.service.ts b/btc-UI/src/app/components/selection.service/label-restriction.service.ts index a284615..b831dbd 100644 --- a/btc-UI/src/app/components/selection.service/label-restriction.service.ts +++ b/btc-UI/src/app/components/selection.service/label-restriction.service.ts @@ -1,4 +1,3 @@ -// label-restriction.service.ts import { Injectable } from '@angular/core'; import { SelectionData } from '../selection.service/selection.service'; @@ -22,15 +21,16 @@ export class LabelRestrictionService { // accept optional currentLabel getBlockedLabels(selections: SelectionData[], currentLabel?: string | null): Set { const selectedGroups = new Set(); + const multiLegLabels = ['TBP', 'MJP', 'JPP']; - // existing finalized selections + // Existing finalized selections for (const row of selections) { if (row.label && LABEL_TO_GROUP[row.label]) { selectedGroups.add(LABEL_TO_GROUP[row.label]); } } - // also consider in-progress/current label (if provided) + // Also consider in-progress/current label (if provided) if (currentLabel && LABEL_TO_GROUP[currentLabel]) { selectedGroups.add(LABEL_TO_GROUP[currentLabel]); } @@ -52,6 +52,12 @@ export class LabelRestrictionService { } }); } + + // New rule: if any multi-leg label is finalized, block all other multi-leg labels + const hasMultiLeg = selections.some(sel => multiLegLabels.includes(sel.label)); + if (hasMultiLeg) { + multiLegLabels.forEach(label => blockLabels.add(label)); + } } return blockLabels; @@ -71,18 +77,18 @@ export class LabelRestrictionService { ): boolean { const boxedLabels = new Set(['FRP', 'QNP', 'TNP']); - // check finalized selections + // Check finalized selections for (const s of selections) { if (s && s.label && boxedLabels.has(s.label) && !!s.isBoxed) { return true; } } - // check the in-progress/current row if provided + // Check the in-progress/current row if provided if (current && current.label && boxedLabels.has(String(current.label)) && !!current.isBoxed) { return true; } return false; } -} +} \ No newline at end of file diff --git a/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts b/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts index 4811f0a..b1a407a 100755 --- a/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts +++ b/btc-UI/src/app/components/touch-pad-menu/touch-pad-menu.component.ts @@ -53,7 +53,8 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { maxRowsReached: boolean = false; totalAmountLimitReached: boolean = false; showLimitPopup: boolean = false; - disabledLabels: string[] = ['SHW', 'SJP', '.','EXA']; + disabledLabels: string[] = ['SHW', 'SJP', '.', 'EXA']; + private hasFinalizedMultiLeg: boolean = false; // Tracks if a multi-leg pool is finalized // TNP logic tanGroupStage = 0; @@ -113,7 +114,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - // Always prefer rpinfo.structuredRaceCard if present, else fall back to raceCardData key (but still use only structuredRaceCard content) + // Always prefer rpinfo.structuredRaceCard if present, else fall back to raceCardData key const rpinfo = this.safeGetJSON('rpinfo'); if (rpinfo && rpinfo.structuredRaceCard) { this.structuredRaceCard = rpinfo.structuredRaceCard; @@ -145,7 +146,8 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.selectionsSubscription = this.selectionService.selections$.subscribe((selections: SelectionData[]) => { this.currentSelections = selections; - this.maxRowsReached = selections.length >= 5; + // Update maxRowsReached to account for multi-leg finalization + this.maxRowsReached = this.hasFinalizedMultiLeg || selections.length >= 5; const totalAmount = selections.reduce((sum: number, selection: SelectionData) => sum + (selection.total || 0), 0); this.totalAmountLimitReached = totalAmount >= 5000; this.refreshBlockedLabels(this.selectedLabel); @@ -175,7 +177,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.cdr.markForCheck(); }); - // Keep raw storage copy if present (but all logic below uses structuredRaceCard) + // Keep raw storage copy if present const data = localStorage.getItem('raceCardData'); if (data) { try { this.raceCardData = JSON.parse(data); } catch { this.raceCardData = this.raceCardData || {}; } @@ -410,10 +412,10 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { isNumberDisabled(number: number): boolean { // Disable if number not present in actualRunners if (!this.actualRunners.has(number)) return true; - // Disable if total amount limit reached + // Disable if total amount limit reached if (this.totalAmountLimitReached) return true; - // Allow all numbers for TNP when boxed, but disable selected numbers - if (this.selectedLabel === 'TNP' && this.isBoxed) { + // Allow all numbers for TNP when boxed, but disable selected numbers + if (this.selectedLabel === 'TNP' && this.isBoxed) { return this.selectedNumbers.includes(number); } // TNP (unboxed): Disable numbers already selected in the current group @@ -442,6 +444,12 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { return; } + // Prevent selecting multi-leg labels if one is already finalized + if (this.hasFinalizedMultiLeg && this.multiLegLabels.includes(label)) { + console.log('[DEBUG] Cannot select another multi-leg pool until selections are cleared'); + return; + } + this.selectedLabel = label; this.selectedNumbers = []; this.padValue = ''; @@ -497,7 +505,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.selectionService.finalizeCurrentRow(); const currentSelections = this.selectionService.getSelections(); const existingWSP = currentSelections.filter(sel => wspLabels.includes(sel.label)); - if (existingWSP.length === 0) { + if (existingWSP.length === 0 && !this.hasFinalizedMultiLeg) { const blankRows = wspLabels.map(lbl => ({ label: lbl, numbers: [], @@ -704,7 +712,17 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } onPadEnter() { - if (this.maxRowsReached) return; + if (this.maxRowsReached || this.hasFinalizedMultiLeg) return; + + // Prevent ENTER action for multi-leg pools if current row already has a value. + // This ensures "Enter" won't re-run and cause race/leg selection clashes once the value is set. + if (this.multiLegLabels.includes(this.selectedLabel || '')) { + const currentRow = this.selectionService.getCurrentRow(); + if (currentRow && typeof currentRow.value === 'number' && currentRow.value > 0) { + // do nothing (ENTER is disabled for multi-leg after entering one value) + return; + } + } if (!this.canPrint) { this.print(); @@ -715,8 +733,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { const labels = ['WNP', 'SHP', 'PLP']; const targetLabel = labels[this.wspTicketStage]; 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 const currentWSPRow = selections.find(sel => sel.label === targetLabel); if (currentWSPRow) { this.selectedNumbers = [...currentWSPRow.numbers]; @@ -733,11 +750,10 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { this.selectionService.setSelections(updatedSelections); // Call after setSelections this.refreshBlockedLabels(this.selectedLabel); - // Only increment stage if not at the last stage (PLP) - + // Only increment stage if not at the last stage (PLP) if (this.wspTicketStage < 2) { this.wspTicketStage++; - // Update selectedNumbers for the next stage + // Update selectedNumbers for the next stage const nextLabel = labels[this.wspTicketStage]; const nextWSPRow = updatedSelections.find(sel => sel.label === nextLabel); if (nextWSPRow) this.selectedNumbers = [...nextWSPRow.numbers]; @@ -981,7 +997,10 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } updateCanPrint() { - if (this.maxRowsReached) { this.canPrint = false; return; } + if (this.maxRowsReached || this.hasFinalizedMultiLeg) { + this.canPrint = false; + return; + } this.canPrint = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue); if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); @@ -991,19 +1010,17 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { // Add this getter for print button enable logic get canPrintTicket(): boolean { - // At least one valid row in finalized selections or current row - const selections = this.selectionService.getSelections(); - const currentRow = this.selectionService.getCurrentRow(); if (this.selectedLabel === 'WSP') { // For WSP, require all three rows (WNP, SHP, PLP) to have valid numbers and values >= 1 const wspLabels = ['WNP', 'SHP', 'PLP']; - const wspSelections = selections.filter(sel => wspLabels.includes(sel.label)); + const wspSelections = this.selectionService.getSelections().filter(sel => wspLabels.includes(sel.label)); const allWSPRowsValid = wspSelections.length === 3 && wspSelections.every(row => row.label && row.numbers && row.numbers.length > 0 && row.value >= 1 && row.total > 0 ); return this.wspTicketStage === 2 && allWSPRowsValid; } - // For non-WSP, keep existing logic: any valid row enables printing + const selections = this.selectionService.getSelections(); + const currentRow = this.selectionService.getCurrentRow(); const hasValidRow = selections.some( row => !!row.label && !!row.numbers && row.numbers.length > 0 && row.value > 0 && row.total > 0 ) || ( @@ -1012,9 +1029,49 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { return Boolean(hasValidRow); } + // ---------- Updated canEnterRow getter ---------- + get canEnterRow(): boolean { + if (this.maxRowsReached) return false; + + // If multi-leg pool (TBP/MJP/JPP): disable "Enter" once a value is already recorded + if (this.multiLegLabels.includes(this.selectedLabel || '')) { + const currentRow = this.selectionService.getCurrentRow(); + // If a value already entered (>0) then Enter should be disabled; only PRINT allowed. + if (currentRow && typeof currentRow.value === 'number' && currentRow.value > 0) { + return false; + } + // Otherwise allow Enter only when value is valid (1..100) and numbers exist + return !!currentRow.label && + !!currentRow.numbers && + currentRow.numbers.length > 0 && + typeof currentRow.value === 'number' && + currentRow.value >= 1 && + currentRow.value <= 100 && + currentRow.total > 0; + } + + // WSP special-case (existing behavior) + if (this.selectedLabel === 'WSP') { + const isValidPadValue = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue); + const hasNumbers = this.selectedNumbers.length > 0; + return isValidPadValue && hasNumbers; + } + + // Default logic for non-multi, non-WSP labels + const currentRow = this.selectionService.getCurrentRow(); + return !!currentRow.label && + !!currentRow.numbers && + currentRow.numbers.length > 0 && + typeof currentRow.value === 'number' && + currentRow.value >= 1 && + currentRow.value <= 100 && + currentRow.total > 0; + } + print() { const selectionsTotal = this.currentSelections.reduce((sum, sel) => sum + (sel.total || 0), 0); let currentRowAmount = 0; + if (this.multiLegLabels.includes(this.selectedLabel || '')) { const maxLegs = this.getMaxLegs(this.currentPool || ''); const horsesPerLeg = this.multiLegGroups.map((group, index) => { @@ -1032,6 +1089,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { } // Ensure all legs have selections if (horsesPerLeg.some(count => count === 0)) return; + this.hasFinalizedMultiLeg = true; // Mark multi-leg pool as finalized } else { currentRowAmount = this.currentTotal; if (selectionsTotal + currentRowAmount > 5000) { @@ -1098,7 +1156,7 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { if (allRows.length === 0) { console.warn("No valid rows to print."); - this.cdr.markForCheck(); // <-- Ensure UI updates + this.cdr.markForCheck(); return; } @@ -1183,34 +1241,33 @@ export class TouchPadMenuComponent implements OnInit, OnDestroy { }); } - let numbersStr = ''; -if (['FRP', 'QNP', 'TNP'].includes(row.label)) { - if (row.isBoxed) { - // Keep only numeric tokens; stringify when comparing to '#' to avoid number vs string compare. - const actualNumbers = displayNumbers - .filter((n): n is number => typeof n === 'number' && String(n) !== '#') - .map(n => String(n).padStart(2, '0')) - .join(','); - numbersStr = `<<${actualNumbers}>>`; - } else if (['FRP', 'QNP'].includes(row.label)) { - // Convert every token to string for safe comparisons and joining. - const actualNumbers = displayNumbers.map(n => String(n)).filter(s => s !== '#').join(','); - // Safe check for '#' presence in row.numbers by stringifying elements - const rowHasHash = Array.isArray(row.numbers) && row.numbers.some(x => String(x) === '#'); - if (rowHasHash || row.isBoxed) { - numbersStr = `${actualNumbers} - ${actualNumbers}`; - } else { - numbersStr = actualNumbers; - } - } else { - // case: TNP but not boxed (falls through here). Use string comparisons consistently. - numbersStr = displayNumbers.map(n => String(n)).filter(s => s !== '#').join(','); - } -} else { - // Default: non-FRP/QNP/TNP labels - numbersStr = displayNumbers.map(n => String(n)).filter(s => s !== '#').join(','); -} - + let numbersStr = ''; + if (['FRP', 'QNP', 'TNP'].includes(row.label)) { + if (row.isBoxed) { + // Keep only numeric tokens; stringify when comparing to '#' to avoid number vs string compare. + const actualNumbers = displayNumbers + .filter((n): n is number => typeof n === 'number' && String(n) !== '#') + .map(n => String(n).padStart(2, '0')) + .join(','); + numbersStr = `<<${actualNumbers}>>`; + } else if (['FRP', 'QNP'].includes(row.label)) { + // Convert every token to string for safe comparisons and joining. + const actualNumbers = displayNumbers.map(n => String(n)).filter(s => s !== '#').join(','); + // Safe check for '#' presence in row.numbers by stringifying elements + const rowHasHash = Array.isArray(row.numbers) && row.numbers.some(x => String(x) === '#'); + if (rowHasHash || row.isBoxed) { + numbersStr = `${actualNumbers} - ${actualNumbers}`; + } else { + numbersStr = actualNumbers; + } + } else { + // case: TNP but not boxed (falls through here). Use string comparisons consistently. + numbersStr = displayNumbers.map(n => String(n)).filter(s => s !== '#').join(','); + } + } else { + // Default: non-FRP/QNP/TNP labels + numbersStr = displayNumbers.map(n => String(n)).filter(s => s !== '#').join(','); + } const label = displayLabel.padEnd(10); const numbers = numbersStr.padEnd(15); @@ -1231,7 +1288,7 @@ if (['FRP', 'QNP', 'TNP'].includes(row.label)) { return legs.join('/'); } -// helper to pad ticket count to 3 digits (001..999) + // 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'); @@ -1276,15 +1333,15 @@ if (['FRP', 'QNP', 'TNP'].includes(row.label)) { return `${label} ${groupStrs.join('-')}${starPart}`; } - // 🔹 NEW RULE: check for "- -" style duplicate groups - if (filteredGroups.length === 2) { - const g1 = filteredGroups[0].map(normalizeToken); - const g2 = filteredGroups[1].map(normalizeToken); - if (g1.join(',') === g2.join(',')) { - return `${label} <<${g1.join(',')}>>${starPart}`; - } - } - + // 🔹 NEW RULE: check for "- -" style duplicate groups + if (filteredGroups.length === 2) { + const g1 = filteredGroups[0].map(normalizeToken); + const g2 = filteredGroups[1].map(normalizeToken); + if (g1.join(',') === g2.join(',')) { + return `${label} <<${g1.join(',')}>>${starPart}`; + } + } + const singleTokens = filteredGroups[0] || []; if (singleTokens.length >= 2 && singleTokens.length % 2 === 0) { const half = singleTokens.length / 2; @@ -1458,14 +1515,27 @@ if (['FRP', 'QNP', 'TNP'].includes(row.label)) { console.log('Printer payload:', payload); + fetch('http://localhost:9100/print', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }) + // ---------------------sending data to backend --------------------------------- - fetch('http://localhost:9100/print', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }) - - // ---------------------sending data to backend --------------------------------- + .then(response => { + if (!response.ok) { + throw new Error(`Printer error: ${response.status}`); + } + return response.text(); + }) + .then(result => { + console.log("✅ Print successful:", result); + this.erase(); + }) + .catch(error => { + console.error("❌ Print failed:", error); + this.erase(); + }); try { const existingTicketsStr = localStorage.getItem('localTickets'); @@ -1542,6 +1612,7 @@ if (['FRP', 'QNP', 'TNP'].includes(row.label)) { erase() { this.selectionService.clearSelections(); this.resetSelections(); + this.hasFinalizedMultiLeg = false; // Reset multi-leg finalization flag this.refreshBlockedLabels(null); this.cdr.markForCheck(); } @@ -1568,7 +1639,7 @@ if (['FRP', 'QNP', 'TNP'].includes(row.label)) { this.fieldInput = ''; this.fieldFEntered = false; this.wspTicketStage = 0; - // Explicitly reset blocked labels (no current label) + this.hasFinalizedMultiLeg = false; // Reset multi-leg finalization flag this.refreshBlockedLabels(null); this.updateCanPrint(); this.sharedStateService.updateSharedData({ type: 'multiLegPoolEnd', value: null }); @@ -2002,25 +2073,6 @@ if (['FRP', 'QNP', 'TNP'].includes(row.label)) { get dedupedEnabledHorseNumbers(): number[] { return _.uniq(this.enabledHorseNumbers); } - // Update canEnterRow to require value between 1 and 100 - get canEnterRow(): boolean { - if (this.maxRowsReached) return false; - if (this.selectedLabel === 'WSP') { - const isValidPadValue = this.padValue.trim().length > 0 && /^[0-9]+$/.test(this.padValue); - const hasNumbers = this.selectedNumbers.length > 0; - return isValidPadValue && hasNumbers; - } - // Default logic for non-WSP - const currentRow = this.selectionService.getCurrentRow(); - return !!currentRow.label && - !!currentRow.numbers && - currentRow.numbers.length > 0 && - typeof currentRow.value === 'number' && - currentRow.value >= 1 && - currentRow.value <= 100 && - currentRow.total > 0; - } - // add this in the component class (near other helpers) private refreshBlockedLabels(currentLabel?: string | null) { // Pass finalized selections and optionally the in-progress/current label