Fix barcode bet verification

parent 9ed29d92
......@@ -2548,6 +2548,73 @@ class Migration_032_FixExtractionAssociationDefaults(DatabaseMigration):
return False
class Migration_033_AddBarcodeFieldsToBets(DatabaseMigration):
"""Add barcode_standard and barcode_data fields to bets table for barcode verification"""
def __init__(self):
super().__init__("033", "Add barcode_standard and barcode_data fields to bets table")
def up(self, db_manager) -> bool:
"""Add barcode fields to bets table"""
try:
with db_manager.engine.connect() as conn:
# Check if columns already exist
result = conn.execute(text("PRAGMA table_info(bets)"))
columns = [row[1] for row in result.fetchall()]
if 'barcode_standard' not in columns:
# Add barcode_standard column
conn.execute(text("""
ALTER TABLE bets
ADD COLUMN barcode_standard VARCHAR(50)
"""))
logger.info("barcode_standard column added to bets table")
else:
logger.info("barcode_standard column already exists in bets table")
if 'barcode_data' not in columns:
# Add barcode_data column
conn.execute(text("""
ALTER TABLE bets
ADD COLUMN barcode_data VARCHAR(255)
"""))
logger.info("barcode_data column added to bets table")
else:
logger.info("barcode_data column already exists in bets table")
# Add indexes for barcode fields
conn.execute(text("""
CREATE INDEX IF NOT EXISTS ix_bets_barcode_data ON bets(barcode_data)
"""))
conn.execute(text("""
CREATE INDEX IF NOT EXISTS ix_bets_barcode_standard ON bets(barcode_standard)
"""))
# Add unique constraint for barcode data per standard
try:
conn.execute(text("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_bets_barcode ON bets(barcode_data, barcode_standard)
"""))
except Exception as e:
logger.warning(f"Could not create unique constraint on barcode fields: {e}")
conn.commit()
logger.info("Barcode fields added to bets table successfully")
return True
except Exception as e:
logger.error(f"Failed to add barcode fields to bets table: {e}")
return False
def down(self, db_manager) -> bool:
"""Remove barcode fields - SQLite doesn't support DROP COLUMN easily"""
logger.warning("SQLite doesn't support DROP COLUMN - barcode fields will remain")
return True
# Registry of all migrations in order
# Registry of all migrations in order
......@@ -2584,6 +2651,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_030_AddZipValidationStatus(),
Migration_031_AddWinningOutcomesFields(),
Migration_032_FixExtractionAssociationDefaults(),
Migration_033_AddBarcodeFieldsToBets(),
]
......
......@@ -678,15 +678,22 @@ class BetModel(BaseModel):
Index('ix_bets_uuid', 'uuid'),
Index('ix_bets_fixture_id', 'fixture_id'),
Index('ix_bets_created_at', 'created_at'),
Index('ix_bets_barcode_data', 'barcode_data'),
Index('ix_bets_barcode_standard', 'barcode_standard'),
UniqueConstraint('uuid', name='uq_bets_uuid'),
UniqueConstraint('barcode_data', 'barcode_standard', name='uq_bets_barcode'),
)
uuid = Column(String(1024), nullable=False, unique=True, comment='Unique identifier for the bet')
fixture_id = Column(String(255), nullable=False, comment='Reference to fixture_id from matches table')
bet_datetime = Column(DateTime, default=datetime.utcnow, nullable=False, comment='Bet creation timestamp')
paid = Column(Boolean, default=False, nullable=False, comment='Payment status (True if payment received)')
paid_out = Column(Boolean, default=False, nullable=False, comment='Payout status (True if winnings paid out)')
# Barcode fields for verification
barcode_standard = Column(String(50), comment='Barcode standard used (ean13, code128, etc.)')
barcode_data = Column(String(255), comment='Barcode data for verification')
# Relationships
bet_details = relationship('BetDetailModel', back_populates='bet', cascade='all, delete-orphan')
......
......@@ -176,11 +176,19 @@ def bet_details(bet_id):
except (json.JSONDecodeError, TypeError):
winning_outcomes = []
# Get odds for this outcome
odds = 0.0
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
detail_dict = {
'id': detail.id,
'match_id': detail.match_id,
'outcome': detail.outcome,
'amount': float(detail.amount),
'odds': float(odds),
'potential_winning': float(detail.amount) * float(odds),
'result': detail.result,
'match': {
'match_number': match.match_number if match else 'Unknown',
......@@ -625,11 +633,19 @@ def cashier_bet_details(bet_id):
except (json.JSONDecodeError, TypeError):
winning_outcomes = []
# Get odds for this outcome
odds = 0.0
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
detail_dict = {
'id': detail.id,
'match_id': detail.match_id,
'outcome': detail.outcome,
'amount': float(detail.amount),
'odds': float(odds),
'potential_winning': float(detail.amount) * float(odds),
'result': detail.result,
'match': {
'match_number': match.match_number if match else 'Unknown',
......@@ -4140,6 +4156,33 @@ def create_cashier_bet():
)
session.add(bet_detail)
# Generate and store barcode data if enabled
try:
from ..utils.barcode_utils import format_bet_id_for_barcode
# Get barcode configuration
if api_bp.db_manager:
barcode_enabled = api_bp.db_manager.get_config_value('barcode.enabled', False)
barcode_standard = api_bp.db_manager.get_config_value('barcode.standard', 'none')
if barcode_enabled and barcode_standard != 'none':
# Generate barcode data for the bet
barcode_data = format_bet_id_for_barcode(bet_uuid, barcode_standard)
# Update the bet with barcode information
new_bet.barcode_standard = barcode_standard
new_bet.barcode_data = barcode_data
session.commit()
logger.info(f"Generated barcode data for bet {bet_uuid}: {barcode_standard} -> {barcode_data}")
else:
logger.debug(f"Barcode generation disabled or not configured for bet {bet_uuid}")
else:
logger.warning("Database manager not available for barcode generation")
except Exception as barcode_e:
logger.error(f"Failed to generate barcode data for bet {bet_uuid}: {barcode_e}")
# Don't fail the bet creation if barcode generation fails
session.commit()
logger.info(f"Created bet {bet_uuid} with {len(bet_details)} details")
......@@ -4449,7 +4492,7 @@ def verify_bet_details(bet_id):
"""Get bet details for verification - no authentication required"""
try:
from ..database.models import BetModel, BetDetailModel, MatchModel
bet_uuid = str(bet_id)
session = api_bp.db_manager.get_session()
try:
......@@ -4464,12 +4507,84 @@ def verify_bet_details(bet_id):
# Get bet details with match information
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet_uuid).all()
details_data = []
for detail in bet_details:
detail_data = detail.to_dict()
# Get match information
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if match:
detail_data['match'] = {
'match_number': match.match_number,
'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township,
'status': match.status,
'result': match.result
}
else:
detail_data['match'] = None
details_data.append(detail_data)
bet_data['details'] = details_data
bet_data['details_count'] = len(details_data)
# Calculate total amount
total_amount = sum(float(detail.amount) for detail in bet_details)
bet_data['total_amount'] = total_amount
return jsonify({
"success": True,
"bet": bet_data
})
finally:
session.close()
except Exception as e:
logger.error(f"API verify bet details error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/verify-barcode')
def verify_barcode():
"""Get bet details for verification by barcode data - no authentication required"""
try:
from ..database.models import BetModel, BetDetailModel, MatchModel
# Get barcode data from query parameters
barcode_data = request.args.get('data', '').strip()
barcode_standard = request.args.get('standard', '').strip()
if not barcode_data:
return jsonify({"error": "Barcode data is required"}), 400
session = api_bp.db_manager.get_session()
try:
# Look up bet by barcode data and standard
query = session.query(BetModel).filter_by(barcode_data=barcode_data)
# If standard is provided, also filter by it
if barcode_standard:
query = query.filter_by(barcode_standard=barcode_standard)
bet = query.first()
if not bet:
return jsonify({"error": "Bet not found for this barcode"}), 404
bet_data = bet.to_dict()
bet_data['paid'] = bet.paid # Include paid status
# Get bet details with match information
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
details_data = []
total_amount = 0.0
for detail in bet_details:
detail_data = detail.to_dict()
total_amount += float(detail.amount)
# Get match information
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if match:
......@@ -4498,7 +4613,7 @@ def verify_bet_details(bet_id):
'cancelled': 0,
'winnings': 0.0
}
overall_status = 'pending'
for detail in bet_details:
if detail.result == 'pending':
......@@ -4510,7 +4625,7 @@ def verify_bet_details(bet_id):
results['lost'] += 1
elif detail.result == 'cancelled':
results['cancelled'] += 1
# Determine overall status
if results['pending'] == 0:
if results['won'] > 0 and results['lost'] == 0:
......@@ -4519,7 +4634,7 @@ def verify_bet_details(bet_id):
overall_status = 'lost'
elif results['cancelled'] > 0:
overall_status = 'cancelled'
bet_data['overall_status'] = overall_status
bet_data['results'] = results
......
......@@ -445,6 +445,8 @@
"venue": "{{ detail.match.venue_kampala_township if detail.match else 'Unknown' }}",
"outcome": "{{ detail.outcome }}",
"amount": {{ detail.amount|round(2) }},
"odds": {{ detail.odds|round(2) }},
"potential_winning": {{ detail.potential_winning|round(2) }},
"result": "{{ detail.result }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
......@@ -628,6 +630,10 @@ function generateReceiptHtml(betData) {
<span>OUTCOME: ${detail.outcome}</span>
<span>${formatCurrency(parseFloat(detail.amount))}</span>
</div>
<div class="receipt-bet-line">
<span>ODDS: ${parseFloat(detail.odds).toFixed(2)}</span>
<span>POTENTIAL WIN: ${formatCurrency(parseFloat(detail.potential_winning))}</span>
</div>
<div class="receipt-status">
STATUS: ${detail.result.toUpperCase()}
</div>
......@@ -774,106 +780,212 @@ function generateVerificationCodes(betUuid) {
}
function printThermalReceipt() {
const printContent = document.getElementById('thermal-receipt').innerHTML;
const printWindow = window.open('', '', 'height=600,width=400');
printWindow.document.write(`
<html>
<head>
<title>Betting Receipt</title>
<style>
@media print {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 11px;
// Mark bet as paid before printing
markBetAsPaidForPrinting(window.betData.uuid).then(() => {
const printContent = document.getElementById('thermal-receipt').innerHTML;
const printWindow = window.open('', '', 'height=600,width=400');
printWindow.document.write(`
<html>
<head>
<title>Betting Receipt</title>
<style>
@media print {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 10px;
}
.receipt-bet-item { margin-bottom: 8px; }
.receipt-match { font-weight: bold; font-size: 12px; text-align: center; }
.receipt-match-details { font-size: 10px; text-align: center; margin-bottom: 2px; }
.receipt-venue { font-size: 9px; text-align: center; margin-bottom: 3px; }
.receipt-status { font-size: 9px; text-align: center; margin-top: 2px; }
.receipt-total { border-top: 1px solid #000; padding-top: 5px; font-weight: bold; }
.receipt-verification { text-align: center; margin: 10px 0; }
.receipt-qr, .receipt-barcode { margin: 5px 0; }
.qr-image { width: 80px; height: 80px; }
.qr-text, .barcode-text { font-size: 9px; margin-top: 3px; }
.barcode-img { max-width: 120px; height: auto; }
.receipt-footer { text-align: center; font-size: 9px; margin-top: 10px; border-top: 1px solid #000; padding-top: 5px; }
.receipt-timestamp { margin-top: 5px; font-size: 8px; }
}
.receipt-bet-item { margin-bottom: 8px; }
.receipt-match { font-weight: bold; font-size: 12px; text-align: center; }
.receipt-match-details { font-size: 10px; text-align: center; margin-bottom: 2px; }
.receipt-venue { font-size: 9px; text-align: center; margin-bottom: 3px; }
.receipt-status { font-size: 9px; text-align: center; margin-top: 2px; }
.receipt-total { border-top: 1px solid #000; padding-top: 5px; font-weight: bold; }
.receipt-verification { text-align: center; margin: 10px 0; }
.receipt-qr, .receipt-barcode { margin: 5px 0; }
.qr-image { width: 80px; height: 80px; }
.qr-text, .barcode-text { font-size: 9px; margin-top: 3px; }
.barcode-img { max-width: 120px; height: auto; }
.receipt-footer { text-align: center; font-size: 9px; margin-top: 10px; border-top: 1px solid #000; padding-top: 5px; }
.receipt-timestamp { margin-top: 5px; font-size: 8px; }
}
body {
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
font-size: 11px;
letter-spacing: -1px;
}
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 15px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 12px;
}
.receipt-bet-item { margin-bottom: 12px; }
.receipt-match { font-weight: bold; font-size: 13px; text-align: center; }
.receipt-match-details { font-size: 11px; text-align: center; margin-bottom: 3px; }
.receipt-venue { font-size: 10px; text-align: center; margin-bottom: 4px; color: #666; }
.receipt-status { font-size: 10px; text-align: center; margin-top: 3px; font-weight: bold; }
.receipt-total {
border-top: 2px solid #000;
padding-top: 8px;
font-weight: bold;
font-size: 14px;
}
.receipt-verification { text-align: center; margin: 15px 0; }
.receipt-qr, .receipt-barcode { margin: 8px 0; }
.qr-image { width: 100px; height: 100px; border: 1px solid #ccc; }
.qr-text, .barcode-text { font-size: 10px; margin-top: 5px; }
.barcode-img { max-width: 150px; height: auto; border: 1px solid #ccc; }
.receipt-footer {
text-align: center;
font-size: 10px;
margin-top: 15px;
border-top: 1px solid #000;
padding-top: 8px;
}
.receipt-timestamp { margin-top: 8px; font-size: 9px; color: #666; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Wait for images to load then print
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 500);
body {
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
font-size: 11px;
letter-spacing: -1px;
}
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 15px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 11px;
}
.receipt-bet-item { margin-bottom: 12px; }
.receipt-match { font-weight: bold; font-size: 13px; text-align: center; }
.receipt-match-details { font-size: 11px; text-align: center; margin-bottom: 3px; }
.receipt-venue { font-size: 10px; text-align: center; margin-bottom: 4px; color: #666; }
.receipt-status { font-size: 10px; text-align: center; margin-top: 3px; font-weight: bold; }
.receipt-total {
border-top: 2px solid #000;
padding-top: 8px;
font-weight: bold;
font-size: 14px;
}
.receipt-verification { text-align: center; margin: 15px 0; }
.receipt-qr, .receipt-barcode { margin: 8px 0; }
.qr-image { width: 100px; height: 100px; border: 1px solid #ccc; }
.qr-text, .barcode-text { font-size: 10px; margin-top: 5px; }
.barcode-img { max-width: 150px; height: auto; border: 1px solid #ccc; }
.receipt-footer {
text-align: center;
font-size: 10px;
margin-top: 15px;
border-top: 1px solid #000;
padding-top: 8px;
}
.receipt-timestamp { margin-top: 8px; font-size: 9px; color: #666; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Wait for images to load then print
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 500);
}).catch(error => {
console.error('Failed to mark bet as paid:', error);
// Still print even if marking as paid fails
const printContent = document.getElementById('thermal-receipt').innerHTML;
const printWindow = window.open('', '', 'height=600,width=400');
printWindow.document.write(`
<html>
<head>
<title>Betting Receipt</title>
<style>
@media print {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 11px;
}
.receipt-bet-item { margin-bottom: 8px; }
.receipt-match { font-weight: bold; font-size: 12px; text-align: center; }
.receipt-match-details { font-size: 10px; text-align: center; margin-bottom: 2px; }
.receipt-venue { font-size: 9px; text-align: center; margin-bottom: 3px; }
.receipt-status { font-size: 9px; text-align: center; margin-top: 2px; }
.receipt-total { border-top: 1px solid #000; padding-top: 5px; font-weight: bold; }
.receipt-verification { text-align: center; margin: 10px 0; }
.receipt-qr, .receipt-barcode { margin: 5px 0; }
.qr-image { width: 80px; height: 80px; }
.qr-text, .barcode-text { font-size: 9px; margin-top: 3px; }
.barcode-img { max-width: 120px; height: auto; }
.receipt-footer { text-align: center; font-size: 9px; margin-top: 10px; border-top: 1px solid #000; padding-top: 5px; }
.receipt-timestamp { margin-top: 5px; font-size: 8px; }
}
body {
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
font-size: 11px;
letter-spacing: -1px;
}
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 15px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 12px;
}
.receipt-bet-item { margin-bottom: 12px; }
.receipt-match { font-weight: bold; font-size: 13px; text-align: center; }
.receipt-match-details { font-size: 11px; text-align: center; margin-bottom: 3px; }
.receipt-venue { font-size: 10px; text-align: center; margin-bottom: 4px; color: #666; }
.receipt-status { font-size: 10px; text-align: center; margin-top: 3px; font-weight: bold; }
.receipt-total {
border-top: 2px solid #000;
padding-top: 8px;
font-weight: bold;
font-size: 14px;
}
.receipt-verification { text-align: center; margin: 15px 0; }
.receipt-qr, .receipt-barcode { margin: 8px 0; }
.qr-image { width: 100px; height: 100px; border: 1px solid #ccc; }
.qr-text, .barcode-text { font-size: 10px; margin-top: 5px; }
.barcode-img { max-width: 150px; height: auto; border: 1px solid #ccc; }
.receipt-footer {
text-align: center;
font-size: 10px;
margin-top: 15px;
border-top: 1px solid #000;
padding-top: 8px;
}
.receipt-timestamp { margin-top: 8px; font-size: 9px; color: #666; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Wait for images to load then print
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 500);
});
}
function deleteBetDetail(detailId) {
......@@ -992,10 +1104,35 @@ function markBetAsPaid(betUuid) {
}
}
function markBetAsPaidForPrinting(betUuid) {
return fetch(`/api/bets/${betUuid}/mark-paid`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Bet marked as paid for printing');
} else {
throw new Error(data.error || 'Failed to mark bet as paid');
}
});
}
function directPrintBet(betId) {
// Use the global bet data for direct printing
const receiptHtml = generateReceiptHtml(window.betData);
printDirectly(receiptHtml);
// Mark bet as paid before printing
markBetAsPaidForPrinting(betId).then(() => {
// Use the global bet data for direct printing
const receiptHtml = generateReceiptHtml(window.betData);
printDirectly(receiptHtml);
}).catch(error => {
console.error('Failed to mark bet as paid:', error);
// Still print even if marking as paid fails
const receiptHtml = generateReceiptHtml(window.betData);
printDirectly(receiptHtml);
});
}
function printDirectly(printContent) {
......
......@@ -445,6 +445,8 @@
"venue": "{{ detail.match.venue_kampala_township if detail.match else 'Unknown' }}",
"outcome": "{{ detail.outcome }}",
"amount": {{ detail.amount|round(2) }},
"odds": {{ detail.odds|round(2) }},
"potential_winning": {{ detail.potential_winning|round(2) }},
"result": "{{ detail.result }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
......@@ -685,6 +687,10 @@ function generateReceiptHtml(betData) {
<span>OUTCOME: ${detail.outcome}</span>
<span>${formatCurrency(parseFloat(detail.amount))}</span>
</div>
<div class="receipt-bet-line">
<span>ODDS: ${parseFloat(detail.odds).toFixed(2)}</span>
<span>POTENTIAL WIN: ${formatCurrency(parseFloat(detail.potential_winning))}</span>
</div>
<div class="receipt-status">
STATUS: ${detail.result.toUpperCase()}
</div>
......@@ -821,106 +827,212 @@ function generateVerificationCodes(betUuid) {
}
function printThermalReceipt() {
const printContent = document.getElementById('thermal-receipt').innerHTML;
const printWindow = window.open('', '', 'height=600,width=400');
printWindow.document.write(`
<html>
<head>
<title>Betting Receipt</title>
<style>
@media print {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
// Mark bet as paid before printing
markBetAsPaidForPrinting(window.betData.uuid).then(() => {
const printContent = document.getElementById('thermal-receipt').innerHTML;
const printWindow = window.open('', '', 'height=600,width=400');
printWindow.document.write(`
<html>
<head>
<title>Betting Receipt</title>
<style>
@media print {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 10px;
}
.receipt-bet-item { margin-bottom: 8px; }
.receipt-match { font-weight: bold; font-size: 12px; text-align: center; }
.receipt-match-details { font-size: 10px; text-align: center; margin-bottom: 2px; }
.receipt-venue { font-size: 9px; text-align: center; margin-bottom: 3px; }
.receipt-status { font-size: 9px; text-align: center; margin-top: 2px; }
.receipt-total { border-top: 1px solid #000; padding-top: 5px; font-weight: bold; }
.receipt-verification { text-align: center; margin: 10px 0; }
.receipt-qr, .receipt-barcode { margin: 5px 0; text-align: center; }
.qr-image { width: 80px; height: 80px; }
.qr-text, .barcode-text { font-size: 9px; margin-top: 3px; }
.barcode-img { width: auto; height: auto; max-width: 150px; }
.receipt-footer { text-align: center; font-size: 9px; margin-top: 10px; border-top: 1px solid #000; padding-top: 5px; }
.receipt-timestamp { margin-top: 5px; font-size: 8px; }
}
body {
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
font-size: 11px;
letter-spacing: -1px;
}
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 15px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 11px;
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 11px;
}
.receipt-bet-item { margin-bottom: 8px; }
.receipt-match { font-weight: bold; font-size: 12px; text-align: center; }
.receipt-match-details { font-size: 10px; text-align: center; margin-bottom: 2px; }
.receipt-venue { font-size: 9px; text-align: center; margin-bottom: 3px; }
.receipt-status { font-size: 9px; text-align: center; margin-top: 2px; }
.receipt-total { border-top: 1px solid #000; padding-top: 5px; font-weight: bold; }
.receipt-verification { text-align: center; margin: 10px 0; }
.receipt-qr, .receipt-barcode { margin: 5px 0; text-align: center; }
.qr-image { width: 80px; height: 80px; }
.qr-text, .barcode-text { font-size: 9px; margin-top: 3px; }
.barcode-img { width: auto; height: auto; max-width: 150px; }
.receipt-footer { text-align: center; font-size: 9px; margin-top: 10px; border-top: 1px solid #000; padding-top: 5px; }
.receipt-timestamp { margin-top: 5px; font-size: 8px; }
}
body {
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
font-size: 11px;
letter-spacing: -1px;
}
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 15px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 12px;
}
.receipt-bet-item { margin-bottom: 12px; }
.receipt-match { font-weight: bold; font-size: 13px; text-align: center; }
.receipt-match-details { font-size: 11px; text-align: center; margin-bottom: 3px; }
.receipt-venue { font-size: 10px; text-align: center; margin-bottom: 4px; color: #666; }
.receipt-status { font-size: 10px; text-align: center; margin-top: 3px; font-weight: bold; }
.receipt-total {
border-top: 2px solid #000;
padding-top: 8px;
font-weight: bold;
font-size: 14px;
}
.receipt-verification { text-align: center; margin: 15px 0; }
.receipt-qr, .receipt-barcode { margin: 8px 0; text-align: center; }
.qr-image { width: 100px; height: 100px; border: 1px solid #ccc; }
.qr-text, .barcode-text { font-size: 10px; margin-top: 5px; }
.barcode-img { width: auto; height: auto; max-width: 200px; border: 1px solid #ccc; }
.receipt-footer {
text-align: center;
font-size: 10px;
margin-top: 15px;
border-top: 1px solid #000;
padding-top: 8px;
}
.receipt-timestamp { margin-top: 8px; font-size: 9px; color: #666; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Wait for images to load then print
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 500);
.receipt-bet-item { margin-bottom: 12px; }
.receipt-match { font-weight: bold; font-size: 13px; text-align: center; }
.receipt-match-details { font-size: 11px; text-align: center; margin-bottom: 3px; }
.receipt-venue { font-size: 10px; text-align: center; margin-bottom: 4px; color: #666; }
.receipt-status { font-size: 10px; text-align: center; margin-top: 3px; font-weight: bold; }
.receipt-total {
border-top: 2px solid #000;
padding-top: 8px;
font-weight: bold;
font-size: 14px;
}
.receipt-verification { text-align: center; margin: 15px 0; }
.receipt-qr, .receipt-barcode { margin: 8px 0; text-align: center; }
.qr-image { width: 100px; height: 100px; border: 1px solid #ccc; }
.qr-text, .barcode-text { font-size: 10px; margin-top: 5px; }
.barcode-img { width: auto; height: auto; max-width: 200px; border: 1px solid #ccc; }
.receipt-footer {
text-align: center;
font-size: 10px;
margin-top: 15px;
border-top: 1px solid #000;
padding-top: 8px;
}
.receipt-timestamp { margin-top: 8px; font-size: 9px; color: #666; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Wait for images to load then print
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 500);
}).catch(error => {
console.error('Failed to mark bet as paid:', error);
// Still print even if marking as paid fails
const printContent = document.getElementById('thermal-receipt').innerHTML;
const printWindow = window.open('', '', 'height=600,width=400');
printWindow.document.write(`
<html>
<head>
<title>Betting Receipt</title>
<style>
@media print {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 11px;
}
.receipt-bet-item { margin-bottom: 8px; }
.receipt-match { font-weight: bold; font-size: 12px; text-align: center; }
.receipt-match-details { font-size: 10px; text-align: center; margin-bottom: 2px; }
.receipt-venue { font-size: 9px; text-align: center; margin-bottom: 3px; }
.receipt-status { font-size: 9px; text-align: center; margin-top: 2px; }
.receipt-total { border-top: 1px solid #000; padding-top: 5px; font-weight: bold; }
.receipt-verification { text-align: center; margin: 10px 0; }
.receipt-qr, .receipt-barcode { margin: 5px 0; text-align: center; }
.qr-image { width: 80px; height: 80px; }
.qr-text, .barcode-text { font-size: 9px; margin-top: 3px; }
.barcode-img { width: auto; height: auto; max-width: 150px; }
.receipt-footer { text-align: center; font-size: 9px; margin-top: 10px; border-top: 1px solid #000; padding-top: 5px; }
.receipt-timestamp { margin-top: 5px; font-size: 8px; }
}
body {
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
font-size: 11px;
letter-spacing: -1px;
}
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 15px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
font-size: 12px;
}
.receipt-bet-item { margin-bottom: 12px; }
.receipt-match { font-weight: bold; font-size: 13px; text-align: center; }
.receipt-match-details { font-size: 11px; text-align: center; margin-bottom: 3px; }
.receipt-venue { font-size: 10px; text-align: center; margin-bottom: 4px; color: #666; }
.receipt-status { font-size: 10px; text-align: center; margin-top: 3px; font-weight: bold; }
.receipt-total {
border-top: 2px solid #000;
padding-top: 8px;
font-weight: bold;
font-size: 14px;
}
.receipt-verification { text-align: center; margin: 15px 0; }
.receipt-qr, .receipt-barcode { margin: 8px 0; text-align: center; }
.qr-image { width: 100px; height: 100px; border: 1px solid #ccc; }
.qr-text, .barcode-text { font-size: 10px; margin-top: 5px; }
.barcode-img { width: auto; height: auto; max-width: 200px; border: 1px solid #ccc; }
.receipt-footer {
text-align: center;
font-size: 10px;
margin-top: 15px;
border-top: 1px solid #000;
padding-top: 8px;
}
.receipt-timestamp { margin-top: 8px; font-size: 9px; color: #666; }
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Wait for images to load then print
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 500);
});
}
function generateBetVerificationQR() {
......@@ -982,10 +1094,35 @@ function markBetAsPaid(betUuid) {
}
}
function markBetAsPaidForPrinting(betUuid) {
return fetch(`/api/cashier/bets/${betUuid}/mark-paid`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Bet marked as paid for printing');
} else {
throw new Error(data.error || 'Failed to mark bet as paid');
}
});
}
function directPrintBet(betId) {
// Use the global bet data for direct printing
const receiptHtml = generateReceiptHtml(window.betData);
printDirectly(receiptHtml);
// Mark bet as paid before printing
markBetAsPaidForPrinting(betId).then(() => {
// Use the global bet data for direct printing
const receiptHtml = generateReceiptHtml(window.betData);
printDirectly(receiptHtml);
}).catch(error => {
console.error('Failed to mark bet as paid:', error);
// Still print even if marking as paid fails
const receiptHtml = generateReceiptHtml(window.betData);
printDirectly(receiptHtml);
});
}
function printDirectly(printContent) {
......
......@@ -501,14 +501,15 @@ function handleBarcodeInput(event) {
function processBarcodeInput() {
const input = document.getElementById('barcode-input');
const barcodeData = input.value.trim();
if (!barcodeData) {
showScannerStatus('Please enter a barcode value', 'warning');
return;
}
console.log('Barcode input detected:', barcodeData);
handleCodeDetected(barcodeData, 'Barcode');
// For manual barcode input, we don't know the standard, so try to verify directly
verifyBarcode(barcodeData);
}
function extractUuidFromBarcode(barcodeData) {
......@@ -550,6 +551,29 @@ function verifyBet(betUuid) {
});
}
function verifyBarcode(barcodeData, barcodeStandard = null) {
const params = new URLSearchParams({
data: barcodeData
});
if (barcodeStandard) {
params.append('standard', barcodeStandard);
}
fetch(`/api/verify-barcode?${params}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayBetDetails(data.bet);
} else {
showScannerStatus('Bet not found: ' + (data.error || 'Unknown error'), 'danger');
}
})
.catch(error => {
console.error('Error verifying barcode:', error);
showScannerStatus('Error verifying barcode: ' + error.message, 'danger');
});
}
function displayBetDetails(bet) {
const modalContent = document.getElementById('bet-details-content');
......
......@@ -482,14 +482,15 @@
function processBarcodeInput() {
const input = document.getElementById('barcode-input');
const barcodeData = input.value.trim();
if (!barcodeData) {
showScannerStatus('Please enter a barcode value', 'warning');
return;
}
console.log('Barcode input detected:', barcodeData);
handleCodeDetected(barcodeData, 'Barcode');
// For manual barcode input, we don't know the standard, so try to verify directly
verifyBarcode(barcodeData);
}
function extractUuidFromBarcode(barcodeData) {
......@@ -531,6 +532,29 @@
});
}
function verifyBarcode(barcodeData, barcodeStandard = null) {
const params = new URLSearchParams({
data: barcodeData
});
if (barcodeStandard) {
params.append('standard', barcodeStandard);
}
fetch(`/api/verify-barcode?${params}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayBetDetails(data.bet);
} else {
showScannerStatus('Bet not found: ' + (data.error || 'Unknown error'), 'danger');
}
})
.catch(error => {
console.error('Error verifying barcode:', error);
showScannerStatus('Error verifying barcode. Please check your connection.', 'danger');
});
}
function displayBetDetails(bet) {
const modalContent = document.getElementById('bet-details-content');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment