Files
quality_app-v2/app/templates/modules/quality/fg_scan.html
Quality App Developer b15cc93b9d FG Scan form validation improvements with warehouse module updates
- Fixed 3 JavaScript syntax errors in fg_scan.html (lines 951, 840-950, 1175-1215)
- Restored form field validation with proper null safety checks
- Re-enabled auto-advance between form fields
- Re-enabled CP code auto-complete with hyphen detection
- Updated validation error messages with clear format specifications and examples
- Added autocomplete='off' to all input fields
- Removed auto-prefix correction feature
- Updated warehouse routes and modules for box assignment workflow
- Added/improved database initialization scripts
- Updated requirements.txt dependencies

Format specifications implemented:
- Operator Code: OP + 2 digits (example: OP01, OP99)
- CP Code: CP + 8 digits + hyphen + 4 digits (example: CP00000000-0001)
- OC1/OC2 Codes: OC + 2 digits (example: OC01, OC99)
- Defect Code: 3 digits only
2026-01-30 10:50:06 +02:00

1450 lines
55 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}FG Scan - Quality App{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fg_scan.css') }}">
<script src="https://cdn.jsdelivr.net/npm/qz-tray@2.1.0/qz-tray.js"></script>
{% endblock %}
{% block content %}
<div class="scan-container">
<!-- Form Card -->
<div class="scan-form-card">
<h3 class="form-title">FG Scan Entry</h3>
<form id="scanForm" method="POST" class="scan-form">
<label for="operator_code">Operator Code:</label>
<input type="text" id="operator_code" name="operator_code" required autocomplete="off">
<div class="error-message" id="error_operator_code"></div>
<label for="cp_code">CP Code:</label>
<input type="text" id="cp_code" name="cp_code" required autocomplete="off">
<div class="error-message" id="error_cp_code"></div>
<label for="oc1_code">OC1 Code:</label>
<input type="text" id="oc1_code" name="oc1_code" autocomplete="off">
<div class="error-message" id="error_oc1_code"></div>
<label for="oc2_code">OC2 Code:</label>
<input type="text" id="oc2_code" name="oc2_code" autocomplete="off">
<div class="error-message" id="error_oc2_code"></div>
<label for="defect_code">Defect Code:</label>
<input type="text" id="defect_code" name="defect_code" maxlength="3" autocomplete="off">
<div class="error-message" id="error_defect_code"></div>
<label for="date_time">Date/Time:</label>
<input type="text" id="date_time" name="date_time" readonly>
<!-- Hidden fields for actual date/time submission -->
<input type="hidden" id="date" name="date">
<input type="hidden" id="time" name="time">
<div class="form-buttons">
<button type="submit" class="btn-submit">Submit Scan</button>
<button type="button" class="btn-clear" id="clearOperator">Clear Quality Operator</button>
</div>
<div class="form-options">
<label class="checkbox-label">
<input type="checkbox" id="scanToBoxes" name="scan_to_boxes">
Scan To Boxes
</label>
</div>
</form>
</div>
<!-- Latest Scans Table -->
<div class="scan-table-card">
<h3 class="table-title">Latest Scans</h3>
<div class="table-wrapper">
<table class="scan-table">
<thead>
<tr>
<th>ID</th>
<th>Op Code</th>
<th>CP Code</th>
<th>OC1</th>
<th>OC2</th>
<th>Defect Code</th>
<th>Date</th>
<th>Time</th>
<th>Approved Qty</th>
<th>Rejected Qty</th>
</tr>
</thead>
<tbody id="scansTableBody">
{% if scan_groups %}
{% for scan_group in scan_groups %}
<tr>
<td>{{ scan_group.id }}</td>
<td>{{ scan_group.operator_code }}</td>
<td>{{ scan_group.cp_code }}</td>
<td>{{ scan_group.oc1_code or '-' }}</td>
<td>{{ scan_group.oc2_code or '-' }}</td>
<td>{{ scan_group.defect_code or '-' }}</td>
<td>{{ scan_group.date }}</td>
<td>{{ scan_group.time }}</td>
<td>{{ scan_group.approved_qty }}</td>
<td>{{ scan_group.rejected_qty }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="10" style="text-align: center; padding: 20px; color: var(--text-secondary);">
<i class="fas fa-inbox"></i> No scans recorded yet. Submit a scan to see results here.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Box Assignment Modal -->
<div id="boxAssignmentModal" class="box-modal" style="display: none;">
<div class="box-modal-content">
<div class="modal-header">
<h2>Assign to Box</h2>
<button type="button" class="modal-close" id="closeModal">&times;</button>
</div>
<div class="modal-body">
<!-- Display CP Code -->
<p style="margin-bottom: 20px; font-size: 0.95em; font-weight: 500;">
CP Code: <strong id="modal-cp-code" style="color: #007bff;">-</strong>
</p>
<!-- OPTION 1: Quick Box Creation -->
<div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border: 1px solid #cce7ff; border-radius: 5px;">
<button type="button" id="quickBoxLabel" class="btn"
style="width: 100%; background: #28a745; color: white; padding: 10px; font-size: 1em; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
📦 Quick Box Label Creation
</button>
<p style="font-size: 0.85em; color: #666; margin-top: 8px; text-align: center;">
Creates new box and prints label immediately
</p>
</div>
<!-- SEPARATOR -->
<div style="text-align: center; margin: 20px 0; color: #999; font-size: 0.9em; letter-spacing: 1px;">
━━━━━━━ OR ━━━━━━━
</div>
<!-- OPTION 2: Scan Existing Box -->
<div style="margin: 20px 0;">
<label for="boxNumber" style="font-weight: 600; display: block; margin-bottom: 8px; color: #333;">Scan Box Number:</label>
<input type="text" id="boxNumber" placeholder="Scan or enter box number"
style="width: 100%; padding: 8px; font-size: 1em; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; font-size: 1.1em;">
<p style="font-size: 0.85em; color: #666; margin-top: 5px;">
Scan an existing box label or enter the box number manually
</p>
</div>
</div>
<div class="modal-footer" style="padding: 15px 20px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn-secondary" id="cancelModal" style="padding: 8px 16px;">Skip</button>
<button type="button" class="btn-submit" id="assignToBox" style="padding: 8px 16px;">Assign to Box</button>
</div>
</div>
</div>
<script>
// Global variables
let scanToBoxesEnabled = false;
let currentCpCode = '';
let currentScanId = null;
let qzTrayReady = false;
let cpCodeLastInputTime = null;
// ===== FORM VALIDATION FUNCTION =====
function validateForm() {
const operatorCode = document.getElementById('operator_code').value.trim();
const cpCode = document.getElementById('cp_code').value.trim();
const oc1Code = document.getElementById('oc1_code').value.trim();
const oc2Code = document.getElementById('oc2_code').value.trim();
const defectCode = document.getElementById('defect_code').value.trim();
// Check required fields
if (!operatorCode || !operatorCode.startsWith('OP') || operatorCode.length !== 4) {
console.log('❌ Operator code validation failed');
return false;
}
if (!cpCode || !cpCode.startsWith('CP') || cpCode.length !== 15) {
console.log('❌ CP code validation failed');
return false;
}
if (!oc1Code || !oc1Code.startsWith('OC') || oc1Code.length !== 4) {
console.log('❌ OC1 code validation failed');
return false;
}
if (!oc2Code || !oc2Code.startsWith('OC') || oc2Code.length !== 4) {
console.log('❌ OC2 code validation failed');
return false;
}
if (!defectCode || !/^\d{3}$/.test(defectCode)) {
console.log('❌ Defect code validation failed');
return false;
}
console.log('✅ All validations passed');
return true;
}
// ===== RESTORE CHECKBOX STATE FROM LOCALSTORAGE =====
const savedCheckboxState = localStorage.getItem('scan_to_boxes_enabled');
if (savedCheckboxState === 'true') {
scanToBoxesEnabled = true;
}
// Get form input references FIRST (before using them) - with null safety check
const operatorCodeInput = document.getElementById('operator_code');
const cpCodeInput = document.getElementById('cp_code');
const oc1CodeInput = document.getElementById('oc1_code');
const oc2CodeInput = document.getElementById('oc2_code');
const defectCodeInput = document.getElementById('defect_code');
// Restore checkbox visual state (must be done after DOM is ready)
// Use a small delay to ensure the checkbox element exists
setTimeout(function() {
const scanToBoxesCheckbox = document.getElementById('scanToBoxes');
if (scanToBoxesCheckbox) {
scanToBoxesCheckbox.checked = scanToBoxesEnabled;
// Also update the display of quickBoxSection if it exists
const quickBoxSection = document.getElementById('quickBoxSection');
if (quickBoxSection) {
quickBoxSection.style.display = scanToBoxesEnabled ? 'block' : 'none';
}
console.log('✅ Checkbox state restored:', scanToBoxesEnabled);
}
}, 100);
// Safety check - ensure all form inputs are available
if (!operatorCodeInput || !cpCodeInput || !oc1CodeInput || !oc2CodeInput || !defectCodeInput) {
console.error('❌ Error: Required form inputs not found in DOM');
console.log('operatorCodeInput:', operatorCodeInput);
console.log('cpCodeInput:', cpCodeInput);
console.log('oc1CodeInput:', oc1CodeInput);
console.log('oc2CodeInput:', oc2CodeInput);
console.log('defectCodeInput:', defectCodeInput);
}
// Initialize QZ Tray only when needed (lazy loading)
// Don't connect on page load - only connect when user enables "Scan To Boxes"
function initializeQzTray() {
if (typeof qz === 'undefined') {
console.log(' QZ Tray library not loaded');
return false;
}
try {
qz.security.setSignaturePromise(function(toSign) {
return new Promise(function(resolve, reject) {
// For development, we'll allow unsigned requests
resolve();
});
});
qz.websocket.connect().then(function() {
qzTrayReady = true;
console.log('✅ QZ Tray connected successfully');
}).catch(function(err) {
console.log(' QZ Tray connection failed:', err);
qzTrayReady = false;
});
return true;
} catch(err) {
console.log(' QZ Tray initialization error:', err);
return false;
}
}
// Update date/time display
function updateDateTime() {
const now = new Date();
const dateStr = now.toLocaleDateString('en-US');
const timeStr = now.toLocaleTimeString('en-US', { hour12: true });
const dateTimeInput = document.getElementById('date_time');
if (dateTimeInput) {
dateTimeInput.value = dateStr + ' ' + timeStr;
}
}
updateDateTime();
setInterval(updateDateTime, 1000);
// Load operator code from localStorage
function loadOperatorCode() {
if (!operatorCodeInput) return;
const saved = localStorage.getItem('quality_operator_code');
if (saved) {
operatorCodeInput.value = saved;
}
}
// Check if we need to clear fields after a successful submission
const shouldClearAfterSubmit = localStorage.getItem('fg_scan_clear_after_submit');
if (shouldClearAfterSubmit === 'true' && cpCodeInput && oc1CodeInput && oc2CodeInput && defectCodeInput) {
// Clear the flag
localStorage.removeItem('fg_scan_clear_after_submit');
localStorage.removeItem('fg_scan_last_cp');
localStorage.removeItem('fg_scan_last_defect');
// Clear CP code, OC1, OC2, and defect code for next scan (NOT operator code)
cpCodeInput.value = '';
oc1CodeInput.value = '';
oc2CodeInput.value = '';
defectCodeInput.value = '';
// Show success indicator
setTimeout(function() {
// Focus on CP code field for next scan
if (cpCodeInput) {
cpCodeInput.focus();
}
// Add visual feedback - small notification on right side
const successIndicator = document.createElement('div');
successIndicator.style.cssText = `
position: fixed;
top: 30px;
right: 30px;
background: #4CAF50;
color: white;
padding: 12px 20px;
border-radius: 6px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
font-weight: 600;
font-size: 14px;
white-space: nowrap;
animation: slideInRight 0.3s ease-out, slideOutRight 0.3s ease-in 2.7s forwards;
`;
successIndicator.textContent = '✅ Scan recorded! Ready for next scan';
document.body.appendChild(successIndicator);
// Add keyframe animation if not already added
if (!document.getElementById('fg-scan-notification-styles')) {
const style = document.createElement('style');
style.id = 'fg-scan-notification-styles';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
// Remove success indicator after 3 seconds
setTimeout(function() {
if (successIndicator.parentNode) {
successIndicator.parentNode.removeChild(successIndicator);
}
}, 3000);
}, 100);
}
// Focus on the first empty required field (only if not clearing after submit)
if (shouldClearAfterSubmit !== 'true' && operatorCodeInput && cpCodeInput && oc1CodeInput && oc2CodeInput && defectCodeInput) {
if (!operatorCodeInput.value) {
operatorCodeInput.focus();
} else if (!cpCodeInput.value) {
cpCodeInput.focus();
} else if (!oc1CodeInput.value) {
oc1CodeInput.focus();
} else if (!oc2CodeInput.value) {
oc2CodeInput.focus();
} else {
defectCodeInput.focus();
}
}
loadOperatorCode();
// Create error message elements with null safety checks
let operatorErrorMessage, cpErrorMessage, oc1ErrorMessage, oc2ErrorMessage, defectErrorMessage;
if (operatorCodeInput && operatorCodeInput.parentNode) {
operatorErrorMessage = document.createElement('div');
operatorErrorMessage.className = 'error-message';
operatorErrorMessage.id = 'operator-error';
operatorErrorMessage.textContent = 'Operator code format: OP + 2 digits (example: OP01, OP99)';
operatorCodeInput.parentNode.insertBefore(operatorErrorMessage, operatorCodeInput.nextSibling);
}
if (cpCodeInput && cpCodeInput.parentNode) {
cpErrorMessage = document.createElement('div');
cpErrorMessage.className = 'error-message';
cpErrorMessage.id = 'cp-error';
cpErrorMessage.textContent = 'CP code format: CP + 8 digits + hyphen + 4 digits (example: CP00000000-0001)';
cpCodeInput.parentNode.insertBefore(cpErrorMessage, cpCodeInput.nextSibling);
}
if (oc1CodeInput && oc1CodeInput.parentNode) {
oc1ErrorMessage = document.createElement('div');
oc1ErrorMessage.className = 'error-message';
oc1ErrorMessage.id = 'oc1-error';
oc1ErrorMessage.textContent = 'OC1 code format: OC + 2 digits (example: OC01, OC99)';
oc1CodeInput.parentNode.insertBefore(oc1ErrorMessage, oc1CodeInput.nextSibling);
}
if (oc2CodeInput && oc2CodeInput.parentNode) {
oc2ErrorMessage = document.createElement('div');
oc2ErrorMessage.className = 'error-message';
oc2ErrorMessage.id = 'oc2-error';
oc2ErrorMessage.textContent = 'OC2 code format: OC + 2 digits (example: OC01, OC99)';
oc2CodeInput.parentNode.insertBefore(oc2ErrorMessage, oc2CodeInput.nextSibling);
}
if (defectCodeInput && defectCodeInput.parentNode) {
defectErrorMessage = document.createElement('div');
defectErrorMessage.className = 'error-message';
defectErrorMessage.id = 'defect-error';
defectErrorMessage.textContent = 'Defect code must be a 3-digit number (e.g., 000, 001, 123)';
defectCodeInput.parentNode.insertBefore(defectErrorMessage, defectCodeInput.nextSibling);
}
// Helper function to safely add/remove error classes
function showErrorMessage(element, message) {
if (element) {
element.classList.add('show');
}
}
function hideErrorMessage(element) {
if (element) {
element.classList.remove('show');
}
}
// ===== CP CODE AUTO-COMPLETE LOGIC =====
let cpCodeAutoCompleteTimeout = null;
function autoCompleteCpCode() {
const value = cpCodeInput.value.trim().toUpperCase();
// Only process if it starts with "CP" but is not 15 characters
if (value.startsWith('CP') && value.length < 15 && value.length > 2) {
console.log('Auto-completing CP code:', value);
// Check if there's a hyphen in the value
if (value.includes('-')) {
// Split by hyphen: CP[base]-[suffix]
const parts = value.split('-');
if (parts.length === 2) {
const cpPrefix = parts[0]; // e.g., "CP00002042"
const suffix = parts[1]; // e.g., "1" or "12" or "123" or "3"
console.log('CP prefix:', cpPrefix, 'Suffix:', suffix);
// Always pad the suffix to exactly 4 digits
const paddedSuffix = suffix.padStart(4, '0');
// Construct the complete CP code
const completedCpCode = `${cpPrefix}-${paddedSuffix}`;
console.log('Completed CP code length:', completedCpCode.length, 'Code:', completedCpCode);
// Ensure it's exactly 15 characters
if (completedCpCode.length === 15) {
console.log('✅ Completed CP code:', completedCpCode);
cpCodeInput.value = completedCpCode;
// Show visual feedback
cpCodeInput.style.backgroundColor = '#e8f5e9';
setTimeout(() => {
cpCodeInput.style.backgroundColor = '';
}, 500);
// Move focus to next field (OC1 code)
setTimeout(() => {
oc1CodeInput.focus();
console.log('✅ Auto-completed CP Code and advanced to OC1');
}, 50);
// Show completion notification
showNotification(`✅ CP Code auto-completed: ${completedCpCode}`, 'success');
} else {
console.log('⚠️ Completed code length is not 15:', completedCpCode.length);
}
}
} else {
console.log('⏳ Waiting for hyphen to be entered before auto-completing');
}
} else {
if (value.length >= 15) {
console.log(' CP code is already complete (15 characters)');
}
}
}
if (cpCodeInput) {
cpCodeInput.addEventListener('input', function() {
cpCodeLastInputTime = Date.now();
const currentValue = this.value.trim().toUpperCase();
this.value = currentValue; // Convert to uppercase
// Clear existing timeout
if (cpCodeAutoCompleteTimeout) {
clearTimeout(cpCodeAutoCompleteTimeout);
}
console.log('CP Code input changed:', currentValue);
// Validate CP code format: CP + 8 digits + hyphen + 4 digits = 15 chars
if (currentValue.length >= 2 && !currentValue.startsWith('CP')) {
showErrorMessage(cpErrorMessage);
this.setCustomValidity('Must start with CP');
} else {
hideErrorMessage(cpErrorMessage);
this.setCustomValidity('');
// Auto-advance when field is complete and valid (exactly 15 chars: CP[8]-[4])
if (currentValue.length === 15 && currentValue.startsWith('CP')) {
setTimeout(() => {
if (oc1CodeInput) oc1CodeInput.focus();
console.log('✅ Auto-advanced from CP Code to OC1 Code');
}, 50);
}
}
// If hyphen is present and value is less than 15 chars, process immediately for auto-complete
if (currentValue.includes('-') && currentValue.length < 15) {
console.log('Hyphen detected, checking for auto-complete');
cpCodeAutoCompleteTimeout = setTimeout(() => {
console.log('Processing auto-complete after hyphen');
autoCompleteCpCode();
}, 500);
} else if (currentValue.length < 15 && currentValue.startsWith('CP')) {
// Set normal 2-second timeout only when no hyphen yet
cpCodeAutoCompleteTimeout = setTimeout(() => {
console.log('2-second timeout triggered for CP code');
autoCompleteCpCode();
}, 2000);
}
});
}
// Also trigger auto-complete when focus leaves the field (blur event)
if (cpCodeInput) {
cpCodeInput.addEventListener('blur', function() {
console.log('CP Code blur event triggered with value:', this.value);
if (cpCodeAutoCompleteTimeout) {
clearTimeout(cpCodeAutoCompleteTimeout);
}
autoCompleteCpCode();
});
}
// Prevent leaving CP code field if invalid
if (cpCodeInput) {
cpCodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('CP')) {
showErrorMessage(cpErrorMessage);
this.setCustomValidity('Must start with CP');
// Return focus to this field
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
}
// Prevent Tab/Enter from moving to next field if CP code is invalid
if (cpCodeInput) {
cpCodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('CP')) {
e.preventDefault();
showErrorMessage(cpErrorMessage);
this.setCustomValidity('Must start with CP');
this.select();
}
});
}
// Prevent focusing on CP code if operator code is invalid
if (cpCodeInput && operatorCodeInput) {
cpCodeInput.addEventListener('focus', function(e) {
if (operatorCodeInput.value.length > 0 && !operatorCodeInput.value.startsWith('OP')) {
e.preventDefault();
showErrorMessage(operatorErrorMessage);
operatorCodeInput.focus();
operatorCodeInput.select();
}
});
}
// ===== OPERATOR CODE VALIDATION =====
if (operatorCodeInput) {
operatorCodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('OP')) {
showErrorMessage(operatorErrorMessage);
this.setCustomValidity('Must start with OP');
} else {
hideErrorMessage(operatorErrorMessage);
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 4 && value.startsWith('OP')) {
setTimeout(() => {
if (cpCodeInput) cpCodeInput.focus();
console.log('✅ Auto-advanced from Operator Code to CP Code');
}, 50);
}
}
});
}
if (operatorCodeInput) {
operatorCodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('OP')) {
showErrorMessage(operatorErrorMessage);
this.setCustomValidity('Must start with OP');
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
}
if (operatorCodeInput) {
operatorCodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OP')) {
e.preventDefault();
showErrorMessage(operatorErrorMessage);
this.setCustomValidity('Must start with OP');
this.select();
}
});
}
// ===== OC1 CODE VALIDATION =====
if (oc1CodeInput) {
oc1CodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('OC')) {
showErrorMessage(oc1ErrorMessage);
this.setCustomValidity('Must start with OC');
} else {
hideErrorMessage(oc1ErrorMessage);
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 4 && value.startsWith('OC')) {
setTimeout(() => {
if (oc2CodeInput) oc2CodeInput.focus();
console.log('✅ Auto-advanced from OC1 Code to OC2 Code');
}, 50);
}
}
});
}
if (oc1CodeInput) {
oc1CodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('OC')) {
showErrorMessage(oc1ErrorMessage);
this.setCustomValidity('Must start with OC');
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
}
if (oc1CodeInput) {
oc1CodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
e.preventDefault();
showErrorMessage(oc1ErrorMessage);
this.setCustomValidity('Must start with OC');
this.select();
}
});
}
if (oc1CodeInput && cpCodeInput) {
oc1CodeInput.addEventListener('focus', function(e) {
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
showErrorMessage(cpErrorMessage);
cpCodeInput.focus();
cpCodeInput.select();
}
});
}
// ===== OC2 CODE VALIDATION =====
if (oc2CodeInput) {
oc2CodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('OC')) {
showErrorMessage(oc2ErrorMessage);
this.setCustomValidity('Must start with OC');
} else {
hideErrorMessage(oc2ErrorMessage);
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 4 && value.startsWith('OC')) {
setTimeout(() => {
if (defectCodeInput) defectCodeInput.focus();
console.log('✅ Auto-advanced from OC2 Code to Defect Code');
}, 50);
}
}
});
}
if (oc2CodeInput) {
oc2CodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('OC')) {
showErrorMessage(oc2ErrorMessage);
this.setCustomValidity('Must start with OC');
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
}
if (oc2CodeInput) {
oc2CodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
e.preventDefault();
showErrorMessage(oc2ErrorMessage);
this.setCustomValidity('Must start with OC');
this.select();
}
});
}
if (oc2CodeInput && cpCodeInput && oc1CodeInput) {
oc2CodeInput.addEventListener('focus', function(e) {
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
showErrorMessage(cpErrorMessage);
cpCodeInput.focus();
cpCodeInput.select();
}
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
e.preventDefault();
showErrorMessage(oc1ErrorMessage);
oc1CodeInput.focus();
oc1CodeInput.select();
}
});
}
// ===== DEFECT CODE VALIDATION =====
if (defectCodeInput) {
defectCodeInput.addEventListener('input', function() {
// Remove any non-digit characters
this.value = this.value.replace(/\D/g, '');
// Validate if it's a valid 3-digit number when length is 3
if (this.value.length === 3) {
const isValid = /^\d{3}$/.test(this.value);
if (!isValid) {
showErrorMessage(defectErrorMessage);
this.setCustomValidity('Must be a 3-digit number');
} else {
hideErrorMessage(defectErrorMessage);
this.setCustomValidity('');
}
} else {
hideErrorMessage(defectErrorMessage);
this.setCustomValidity('');
}
// Auto-submit when 3 characters are entered and all validations pass
if (this.value.length === 3) {
// Validate operator code before submitting
if (operatorCodeInput && !operatorCodeInput.value.startsWith('OP')) {
showErrorMessage(operatorErrorMessage);
operatorCodeInput.focus();
operatorCodeInput.setCustomValidity('Must start with OP');
return;
}
// Validate CP code before submitting
if (cpCodeInput && (!cpCodeInput.value.startsWith('CP') || cpCodeInput.value.length !== 15)) {
showErrorMessage(cpErrorMessage);
cpCodeInput.focus();
cpCodeInput.setCustomValidity('Must start with CP and be complete');
return;
}
// Validate OC1 code before submitting
if (oc1CodeInput && !oc1CodeInput.value.startsWith('OC')) {
showErrorMessage(oc1ErrorMessage);
oc1CodeInput.focus();
oc1CodeInput.setCustomValidity('Must start with OC');
return;
}
// Validate OC2 code before submitting
if (oc2CodeInput && !oc2CodeInput.value.startsWith('OC')) {
showErrorMessage(oc2ErrorMessage);
oc2CodeInput.focus();
oc2CodeInput.setCustomValidity('Must start with OC');
return;
}
// Validate defect code is a valid 3-digit number
const isValidDefectCode = /^\d{3}$/.test(this.value);
if (!isValidDefectCode) {
showErrorMessage(defectErrorMessage);
this.focus();
this.setCustomValidity('Must be a 3-digit number');
return;
}
// Clear all custom validity states before submitting
if (operatorCodeInput) operatorCodeInput.setCustomValidity('');
if (cpCodeInput) cpCodeInput.setCustomValidity('');
if (oc1CodeInput) oc1CodeInput.setCustomValidity('');
if (oc2CodeInput) oc2CodeInput.setCustomValidity('');
this.setCustomValidity('');
// ===== TIME FIELD AUTO-UPDATE (CRITICAL) =====
// Update time field to current time before submitting
const timeInput = document.getElementById('date_time');
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const timeValue = `${hours}:${minutes}:${seconds}`;
// Format date as YYYY-MM-DD for database
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const dateValue = `${year}-${month}-${day}`;
// Parse the current datetime display and update just the time part
const dateStr = timeInput.value.split(' ').slice(0, -1).join(' '); // Get date part
timeInput.value = dateStr + ' ' + timeValue;
// Populate hidden date/time fields for form submission
document.getElementById('date').value = dateValue;
document.getElementById('time').value = timeValue;
console.log('✅ Time field updated to:', timeValue);
console.log('✅ Date field set to:', dateValue);
// Save current scan data to localStorage for clearing after reload
localStorage.setItem('fg_scan_clear_after_submit', 'true');
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
// Auto-submit the form - directly trigger submission logic
console.log('Auto-submitting form on 3-digit defect code');
const scanForm = document.getElementById('scanForm');
// Validate the form first
if (!validateForm()) {
console.log('Form validation failed');
return;
}
// Save operator code
localStorage.setItem('quality_operator_code', document.getElementById('operator_code').value.trim());
const formData = new FormData(scanForm);
// If AJAX is needed (scan to boxes)
if (scanToBoxesEnabled) {
console.log('Using AJAX submission (scanToBoxesEnabled = true)');
fetch('{{ url_for("quality.fg_scan") }}', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Scan saved successfully!', 'success');
// Store CP code and scan ID for modal
const cpCodeElement = document.getElementById('cp_code');
if (cpCodeElement) {
currentCpCode = cpCodeElement.value.trim();
}
currentScanId = data.scan_id;
console.log('📌 Scan ID stored:', currentScanId);
// Reset form
resetForm();
// Show box assignment modal with CP code - with null checks
const modalCpCodeEl = document.getElementById('modal-cp-code');
const boxNumberEl = document.getElementById('boxNumber');
const boxAssignmentModalEl = document.getElementById('boxAssignmentModal');
if (modalCpCodeEl) {
modalCpCodeEl.textContent = currentCpCode;
}
if (boxNumberEl) {
boxNumberEl.value = ''; // Clear previous entry
}
if (boxAssignmentModalEl) {
boxAssignmentModalEl.style.display = 'flex';
}
// Focus on box number input
setTimeout(() => {
const boxInput = document.getElementById('boxNumber');
if (boxInput) {
boxInput.focus();
}
}, 100);
} else {
showNotification('❌ Error saving scan', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Error saving scan: ' + error.message, 'error');
});
} else {
console.log('Using regular form submission (scanToBoxesEnabled = false)');
// For non-AJAX submission, use normal form submit which will reload page
scanForm.submit();
}
}
});
}
if (defectCodeInput && cpCodeInput && oc1CodeInput && oc2CodeInput) {
defectCodeInput.addEventListener('focus', function(e) {
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
showErrorMessage(cpErrorMessage);
cpCodeInput.focus();
cpCodeInput.select();
}
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
e.preventDefault();
showErrorMessage(oc1ErrorMessage);
oc1CodeInput.focus();
oc1CodeInput.select();
}
if (oc2CodeInput.value.length > 0 && !oc2CodeInput.value.startsWith('OC')) {
e.preventDefault();
showErrorMessage(oc2ErrorMessage);
oc2CodeInput.focus();
oc2CodeInput.select();
}
});
}
// ===== CLEAR OPERATOR CODE BUTTON =====
const clearOperatorBtn = document.getElementById('clearOperator');
if (clearOperatorBtn && operatorCodeInput) {
clearOperatorBtn.addEventListener('click', function(e) {
e.preventDefault();
operatorCodeInput.value = '';
localStorage.removeItem('quality_operator_code');
operatorCodeInput.focus();
showNotification('Quality Operator Code cleared', 'info');
});
}
// ===== SAVE OPERATOR CODE ON INPUT =====
if (operatorCodeInput) {
operatorCodeInput.addEventListener('input', function() {
if (this.value.startsWith('OP') && this.value.length >= 3) {
localStorage.setItem('quality_operator_code', this.value);
}
});
}
// Form submission
document.getElementById('scanForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!validateForm()) {
return;
}
// Save operator code
localStorage.setItem('quality_operator_code', document.getElementById('operator_code').value.trim());
const formData = new FormData(this);
// If AJAX is needed (scan to boxes)
if (scanToBoxesEnabled) {
fetch('{{ url_for("quality.fg_scan") }}', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Scan saved successfully!', 'success');
// Store CP code for modal
currentCpCode = document.getElementById('cp_code').value.trim();
// Reset form
resetForm();
// Show box assignment modal with CP code
document.getElementById('modal-cp-code').textContent = currentCpCode;
document.getElementById('boxNumber').value = ''; // Clear previous entry
document.getElementById('boxAssignmentModal').style.display = 'flex';
// Focus on box number input
setTimeout(() => {
document.getElementById('boxNumber').focus();
}, 100);
} else {
showNotification('❌ Error saving scan', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Error saving scan: ' + error.message, 'error');
});
} else {
// Regular form submission (non-AJAX)
this.submit();
}
});
// Scan to boxes functionality
document.getElementById('scanToBoxes').addEventListener('change', function() {
scanToBoxesEnabled = this.checked;
// Save checkbox state to localStorage for persistence across page refreshes
localStorage.setItem('scan_to_boxes_enabled', this.checked);
document.getElementById('quickBoxSection').style.display = this.checked ? 'block' : 'none';
if (this.checked) {
// Initialize QZ Tray when user enables the feature
console.log('✅ Scan To Boxes enabled - initializing QZ Tray...');
initializeQzTray();
} else {
console.log(' Scan To Boxes disabled');
}
});
// Quick box label creation - COMPLETE IMPLEMENTATION
document.getElementById('quickBoxLabel').addEventListener('click', async function() {
// Check if scan-to-boxes is enabled
if (!scanToBoxesEnabled) {
showNotification('⚠️ Please enable "Scan to Boxes" first', 'warning');
return;
}
try {
this.disabled = true;
this.textContent = '⏳ Creating...';
// Step 1: Create box in database
console.log('📦 Step 1: Creating new box...');
const createResponse = await fetch('{{ url_for("quality.create_quick_box") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
}).catch(err => {
console.error('Create box endpoint not found, trying alternative URL');
throw err;
});
if (!createResponse.ok) {
throw new Error(`Failed to create box: ${createResponse.statusText}`);
}
const createResult = await createResponse.json();
if (!createResult.success || !createResult.box_number) {
throw new Error(createResult.error || 'Failed to create box');
}
const boxNumber = createResult.box_number;
console.log('✅ Box created:', boxNumber);
showNotification(`✅ Box ${boxNumber} created`, 'success');
// Step 2: Generate PDF label
console.log('📄 Step 2: Generating PDF label...');
const pdfFormData = new FormData();
pdfFormData.append('box_number', boxNumber);
const pdfResponse = await fetch('{{ url_for("quality.generate_box_label_pdf") }}', {
method: 'POST',
body: pdfFormData
});
if (!pdfResponse.ok) {
throw new Error('Failed to generate PDF label');
}
const pdfResult = await pdfResponse.json();
if (!pdfResult.success) {
throw new Error(pdfResult.error || 'Failed to generate PDF');
}
console.log('✅ PDF generated');
// Step 3: Print label via QZ Tray
console.log('🖨️ Step 3: Printing label via QZ Tray...');
try {
// Check QZ Tray connection
if (!window.qz || !window.qz.websocket.isActive()) {
console.log('🔌 Attempting to connect to QZ Tray...');
await window.qz.websocket.connect();
}
// Get printers
const printers = await window.qz.printers.find();
if (printers.length === 0) {
throw new Error('No printers found');
}
// Get default or first available printer
let printer;
try {
printer = await window.qz.printers.getDefault();
} catch (e) {
printer = printers[0];
}
console.log('🖨️ Using printer:', printer);
// Configure print job
const config = window.qz.configs.create(printer, {
scaleContent: false,
rasterize: false,
size: { width: 80, height: 50 },
units: 'mm',
margins: { top: 0, right: 0, bottom: 0, left: 0 }
});
// Prepare PDF data
const printData = [{
type: 'pdf',
format: 'base64',
data: pdfResult.pdf_base64
}];
// Print
await window.qz.print(config, printData);
console.log('✅ Label printed successfully');
showNotification(`✅ Box ${boxNumber} created and label printed!`, 'success');
// Step 4: Show box number (already shown in modal) - with null checks
console.log('📝 Step 4: Box number display...');
const boxNumberInput = document.getElementById('boxNumber');
if (boxNumberInput) {
boxNumberInput.value = boxNumber;
}
} catch (printError) {
console.warn('⚠️ QZ Tray print failed, using fallback:', printError);
showNotification(
`⚠️ Box ${boxNumber} created but QZ Tray print not available.\n` +
`Please check if QZ Tray is running.`,
'warning'
);
// Still populate box number for manual entry - with null checks
const boxNumberInput = document.getElementById('boxNumber');
const boxQtyInput = document.getElementById('boxQty');
if (boxNumberInput) {
boxNumberInput.value = boxNumber;
}
if (boxQtyInput) {
boxQtyInput.value = '1';
}
}
} catch (error) {
console.error('❌ Error:', error);
showNotification(`❌ Error: ${error.message}`, 'error');
} finally {
// Re-enable button
this.disabled = false;
this.textContent = '📦 Quick Box Label Creation';
}
});
// Modal functionality
const closeModalBtn = document.getElementById('closeModal');
if (closeModalBtn) {
closeModalBtn.addEventListener('click', function() {
document.getElementById('boxAssignmentModal').style.display = 'none';
// Reload page to show updated scans table
setTimeout(() => {
location.reload();
}, 500);
});
}
const cancelModalBtn = document.getElementById('cancelModal');
if (cancelModalBtn) {
cancelModalBtn.addEventListener('click', function() {
showNotification('✅ Scan recorded without box assignment', 'success');
document.getElementById('boxAssignmentModal').style.display = 'none';
// Clear stored values
currentCpCode = '';
currentScanId = null;
// Reload page to show updated scans table
setTimeout(() => {
location.reload();
}, 500);
});
}
const assignToBoxBtn = document.getElementById('assignToBox');
if (assignToBoxBtn) {
assignToBoxBtn.addEventListener('click', async function() {
console.log('=== Assign to Box button clicked ===');
// Add null checks for DOM elements
const boxNumberInput = document.getElementById('boxNumber');
if (!boxNumberInput) {
console.error('❌ Modal elements not found');
showNotification('⚠️ Modal elements not properly loaded. Please refresh the page.', 'error');
return;
}
const boxNumber = boxNumberInput.value.trim();
const boxQty = 1; // Always 1 - one scanned item per scan record
console.log('Box Number:', boxNumber);
console.log('Box Quantity:', boxQty);
console.log('Current Scan ID:', currentScanId);
console.log('Current CP Code:', currentCpCode);
if (!boxNumber) {
console.warn('Box number is empty');
showNotification('⚠️ Please enter a box number', 'warning');
return;
}
if (!currentScanId) {
console.error('❌ No scan ID found');
showNotification('⚠️ Error: No scan ID stored. Please submit a scan first.', 'error');
return;
}
try {
this.disabled = true;
this.textContent = '⏳ Assigning...';
console.log('Sending assignment request to backend...');
// Submit box assignment using scan ID
// The backend will retrieve the CP code from the scan record using the scan ID
const response = await fetch('{{ url_for("quality.assign_cp_to_box") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
scan_id: currentScanId,
box_number: boxNumber,
quantity: parseInt(boxQty)
})
});
console.log('Response status:', response.status);
console.log('Response OK:', response.ok);
if (!response.ok) {
const errorText = await response.text();
console.error('Error response body:', errorText);
throw new Error(`Server error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('Response data:', data);
if (data.success) {
console.log('✅ Assignment successful');
// Get CP code from response if available, otherwise use stored value
const cpCodeForMessage = data.cp_code || currentCpCode || 'N/A';
showNotification(
`✅ CP ${cpCodeForMessage} assigned to box ${boxNumber}!`,
'success'
);
// Close modal
const modalElement = document.getElementById('boxAssignmentModal');
if (modalElement) {
modalElement.style.display = 'none';
}
// Clear box inputs with null checks
const boxNumInput = document.getElementById('boxNumber');
if (boxNumInput) {
boxNumInput.value = '';
}
// Clear stored scan ID
currentScanId = null;
currentCpCode = '';
// Reload page to show updated scans table after a brief delay
setTimeout(() => {
location.reload();
}, 1000);
} else {
console.error('Server returned success: false');
throw new Error(data.error || 'Unknown error from server');
}
} catch (error) {
console.error('❌ Error in assignToBox:', error);
showNotification(`❌ Error: ${error.message}`, 'error');
} finally {
this.disabled = false;
this.textContent = 'Assign to Box'; // ← Fixed: was just 'Assign'
}
});
}
// Utility functions
function resetForm() {
document.getElementById('cp_code').value = '';
document.getElementById('oc1_code').value = '';
document.getElementById('oc2_code').value = '';
document.getElementById('defect_code').value = '';
currentCpCode = '';
document.getElementById('cp_code').focus();
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
// Apply inline styles to ensure proper positioning and appearance
notification.style.cssText = `
position: fixed;
top: 30px;
right: 30px;
padding: 12px 20px;
border-radius: 6px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
font-weight: 600;
font-size: 14px;
white-space: nowrap;
opacity: 1;
animation: slideInRight 0.3s ease-out, slideOutRight 0.3s ease-in 2.7s forwards;
`;
document.body.appendChild(notification);
// Ensure animations exist
if (!document.getElementById('notification-styles')) {
const style = document.createElement('style');
style.id = 'notification-styles';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
// Remove notification after animation completes
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
// Dark mode support
function applyDarkModeStyles() {
const isDarkMode = document.body.classList.contains('dark-mode');
if (isDarkMode) {
document.documentElement.style.setProperty('--bg-color', '#1e1e1e');
document.documentElement.style.setProperty('--text-color', '#e0e0e0');
}
}
// Check dark mode on page load
if (document.body.classList.contains('dark-mode')) {
applyDarkModeStyles();
}
// Listen for dark mode changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
applyDarkModeStyles();
}
});
});
observer.observe(document.body, { attributes: true });
</script>
{% endblock %}