Files
quality_app/py_app/app/templates/fg_scan.html
Quality App System 04a37501ec Fix: FG scan form submission hang and add theme toggle functionality
- Added isAutoSubmitting flag to prevent duplicate validation during auto-submit
- Clear all custom validity states before form submission
- Skip duplicate validation in submit handler when auto-submitting
- Added theme toggle functionality directly to fg_scan page (since script.js is excluded)
- Enhanced logging for debugging form submission flow

This fixes the issue where the form would hang and not send data to the database when defect code was auto-submitted.
2026-01-19 21:14:44 +02:00

1242 lines
48 KiB
HTML
Raw Permalink 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 %}Finish Good Scan{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/scan.css') }}">
<!-- QZ Tray for printing - using local patched version for pairing-key authentication -->
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
<style>
.error-message {
color: #ff4444;
font-size: 0.9em;
margin-top: 5px;
display: none;
grid-column: 2 / -1;
}
.error-message.show {
display: block;
}
</style>
<script>
// Global variables for scan-to-boxes feature - must be accessible by all scripts
let scanToBoxesEnabled = false;
let currentCpCode = null;
// Define scan-to-boxes functions at global scope
async function submitScanWithBoxAssignment() {
const form = document.getElementById('fg-scan-form');
const formData = new FormData(form);
console.log('=== submitScanWithBoxAssignment called ===');
console.log('Form data entries:');
for (let [key, value] of formData.entries()) {
console.log(` ${key}: ${value}`);
}
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
if (response.ok) {
currentCpCode = formData.get('cp_code');
const defectCode = formData.get('defect_code') || '000';
console.log('CP Code:', currentCpCode);
console.log('Defect Code:', defectCode);
showNotification('✅ Scan recorded successfully!', 'success');
// Only show box modal if quality code is 000
if (defectCode === '000' || defectCode === '0') {
console.log('Should show box modal');
showBoxModal(currentCpCode);
} else {
console.log('Defect code not 000, reloading page');
setTimeout(() => window.location.reload(), 1000);
}
// Clear form fields (except operator code)
document.getElementById('cp_code').value = '';
document.getElementById('oc1_code').value = '';
document.getElementById('oc2_code').value = '';
document.getElementById('defect_code').value = '';
} else {
console.error('Response not OK');
showNotification('❌ Scan submission failed', 'error');
}
} catch (error) {
console.error('Error in submitScanWithBoxAssignment:', error);
showNotification('❌ Error: ' + error.message, 'error');
}
}
function showBoxModal(cpCode) {
document.getElementById('modal-cp-code').textContent = cpCode;
document.getElementById('box-assignment-modal').style.display = 'block';
document.getElementById('scan-box-input').value = '';
document.getElementById('scan-box-input').focus();
}
function closeBoxModal() {
document.getElementById('box-assignment-modal').style.display = 'none';
window.location.reload();
}
async function assignCpToBox(boxNumber) {
const response = await fetch('/warehouse/assign_cp_to_box', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
box_number: boxNumber,
cp_code: currentCpCode
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to assign CP to box');
}
return await response.json();
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : type === 'warning' ? '#ff9800' : '#2196F3'};
color: white;
padding: 15px 20px;
border-radius: 5px;
z-index: 10001;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
font-weight: bold;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 4000);
}
document.addEventListener('DOMContentLoaded', function() {
// ========== THEME TOGGLE FUNCTIONALITY ==========
const themeToggleButton = document.getElementById('theme-toggle');
const body = document.body;
// Helper function to update the theme toggle button text
function updateThemeToggleButtonText() {
if (themeToggleButton) {
if (body.classList.contains('dark-mode')) {
themeToggleButton.textContent = 'Change to Light Mode';
} else {
themeToggleButton.textContent = 'Change to Dark Mode';
}
}
}
// Check and apply the saved theme from localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
body.classList.toggle('dark-mode', savedTheme === 'dark');
}
// Update the button text based on the current theme
updateThemeToggleButtonText();
// Toggle the theme on button click
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
const isDarkMode = body.classList.toggle('dark-mode');
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
updateThemeToggleButtonText();
});
}
// ========== END THEME TOGGLE ==========
// Load toggle state FIRST
const savedState = localStorage.getItem('scan_to_boxes_enabled');
if (savedState === 'true') {
scanToBoxesEnabled = true;
}
console.log('Initial scanToBoxesEnabled:', scanToBoxesEnabled);
// Flag to prevent duplicate validation during auto-submit
let isAutoSubmitting = false;
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');
const form = document.getElementById('fg-scan-form');
// Set up toggle checkbox
const toggleElement = document.getElementById('scan-to-boxes-toggle');
if (toggleElement) {
toggleElement.checked = scanToBoxesEnabled;
toggleElement.addEventListener('change', function() {
scanToBoxesEnabled = this.checked;
localStorage.setItem('scan_to_boxes_enabled', this.checked);
console.log('Toggle changed - Scan to boxes:', scanToBoxesEnabled);
// Connect or disconnect QZ Tray based on toggle state
if (scanToBoxesEnabled) {
console.log('Scan-to-boxes enabled, connecting QZ Tray...');
if (window.qz && !window.qz.websocket.isActive()) {
window.qz.websocket.connect().then(() => {
console.log('QZ Tray connected');
showNotification('✅ QZ Tray connected for box label printing', 'success');
}).catch(err => {
console.warn('QZ Tray connection failed:', err);
showNotification('⚠️ QZ Tray connection failed. Box labels cannot be printed.', 'warning');
});
}
} else {
console.log('Scan-to-boxes disabled, disconnecting QZ Tray...');
if (window.qz && window.qz.websocket.isActive()) {
window.qz.websocket.disconnect().then(() => {
console.log('QZ Tray disconnected');
showNotification(' QZ Tray disconnected', 'info');
}).catch(err => {
console.warn('QZ Tray disconnect failed:', err);
});
}
}
});
}
// Restore saved operator code from localStorage (only Quality Operator Code)
const savedOperatorCode = localStorage.getItem('fg_scan_operator_code');
if (savedOperatorCode) {
operatorCodeInput.value = savedOperatorCode;
}
// Check if we need to clear fields after a successful submission
const shouldClearAfterSubmit = localStorage.getItem('fg_scan_clear_after_submit');
if (shouldClearAfterSubmit === 'true') {
// 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
cpCodeInput.value = '';
oc1CodeInput.value = '';
oc2CodeInput.value = '';
defectCodeInput.value = '';
// Show success indicator
setTimeout(function() {
// Focus on CP code field for next scan
cpCodeInput.focus();
// Add visual feedback
const successIndicator = document.createElement('div');
successIndicator.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
font-weight: bold;
`;
successIndicator.textContent = '✅ Scan recorded! Ready for next scan';
document.body.appendChild(successIndicator);
// 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') {
if (!operatorCodeInput.value) {
operatorCodeInput.focus();
} else if (!oc1CodeInput.value) {
oc1CodeInput.focus();
} else if (!oc2CodeInput.value) {
oc2CodeInput.focus();
} else if (!cpCodeInput.value) {
cpCodeInput.focus();
} else {
defectCodeInput.focus();
}
}
// Save operator codes to localStorage when they change (only Quality Operator Code)
operatorCodeInput.addEventListener('input', function() {
if (this.value.startsWith('OP') && this.value.length >= 3) {
localStorage.setItem('fg_scan_operator_code', this.value);
}
});
// Create error message element for operator code
const operatorErrorMessage = document.createElement('div');
operatorErrorMessage.className = 'error-message';
operatorErrorMessage.id = 'operator-error';
operatorErrorMessage.textContent = 'Please scan Quality Operator code (must start with OP)';
operatorCodeInput.parentNode.insertBefore(operatorErrorMessage, operatorCodeInput.nextSibling);
// Create error message element for CP code
const cpErrorMessage = document.createElement('div');
cpErrorMessage.className = 'error-message';
cpErrorMessage.id = 'cp-error';
cpErrorMessage.textContent = 'Please scan a valid CP';
cpCodeInput.parentNode.insertBefore(cpErrorMessage, cpCodeInput.nextSibling);
// CP Code Auto-Completion Feature: Pad incomplete CP codes after 2 seconds
let cpCodeLastInputTime = null;
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 (to make total 15 chars: CP[8digits]-[4digits])
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)
oc1CodeInput.focus();
// 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)');
}
}
}
cpCodeInput.addEventListener('input', function() {
cpCodeLastInputTime = Date.now();
const currentValue = this.value.trim().toUpperCase();
// Clear existing timeout
if (cpCodeAutoCompleteTimeout) {
clearTimeout(cpCodeAutoCompleteTimeout);
}
console.log('CP Code input changed:', currentValue);
// If hyphen is present and value is less than 15 chars, process immediately
if (currentValue.includes('-') && currentValue.length < 15) {
console.log('Hyphen detected, checking for auto-complete');
// Set shorter timeout (500ms) when hyphen is present
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)
cpCodeInput.addEventListener('blur', function() {
console.log('CP Code blur event triggered with value:', this.value);
if (cpCodeAutoCompleteTimeout) {
clearTimeout(cpCodeAutoCompleteTimeout);
}
autoCompleteCpCode();
});
// Create error message element for OC1 code
const oc1ErrorMessage = document.createElement('div');
oc1ErrorMessage.className = 'error-message';
oc1ErrorMessage.id = 'oc1-error';
oc1ErrorMessage.textContent = 'Please scan a valid OC (must start with OC)';
oc1CodeInput.parentNode.insertBefore(oc1ErrorMessage, oc1CodeInput.nextSibling);
// Create error message element for OC2 code
const oc2ErrorMessage = document.createElement('div');
oc2ErrorMessage.className = 'error-message';
oc2ErrorMessage.id = 'oc2-error';
oc2ErrorMessage.textContent = 'Please scan a valid OC (must start with OC)';
oc2CodeInput.parentNode.insertBefore(oc2ErrorMessage, oc2CodeInput.nextSibling);
// Create error message element for defect code
const 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);
// Validate operator code on input
operatorCodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('OP')) {
operatorErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OP');
} else {
operatorErrorMessage.classList.remove('show');
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 4 && value.startsWith('OP')) {
cpCodeInput.focus();
}
}
});
// Prevent leaving operator code field if invalid
operatorCodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('OP')) {
operatorErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OP');
// Return focus to this field
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
// Prevent Tab/Enter from moving to next field if operator code is invalid
operatorCodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OP')) {
e.preventDefault();
operatorErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OP');
this.select();
}
});
// Validate CP code on input
cpCodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('CP')) {
cpErrorMessage.classList.add('show');
this.setCustomValidity('Must start with CP');
} else {
cpErrorMessage.classList.remove('show');
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 15 && value.startsWith('CP')) {
oc1CodeInput.focus();
}
}
});
// Prevent leaving CP code field if invalid
cpCodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('CP')) {
cpErrorMessage.classList.add('show');
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
cpCodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('CP')) {
e.preventDefault();
cpErrorMessage.classList.add('show');
this.setCustomValidity('Must start with CP');
this.select();
}
});
// Prevent focusing on CP code if operator code is invalid
cpCodeInput.addEventListener('focus', function(e) {
if (operatorCodeInput.value.length > 0 && !operatorCodeInput.value.startsWith('OP')) {
e.preventDefault();
operatorErrorMessage.classList.add('show');
operatorCodeInput.focus();
operatorCodeInput.select();
}
});
// Prevent focusing on OC1 code if CP code is invalid
oc1CodeInput.addEventListener('focus', function(e) {
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
cpErrorMessage.classList.add('show');
cpCodeInput.focus();
cpCodeInput.select();
}
});
// Validate OC1 code on input
oc1CodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('OC')) {
oc1ErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OC');
} else {
oc1ErrorMessage.classList.remove('show');
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 4 && value.startsWith('OC')) {
oc2CodeInput.focus();
}
}
});
// Prevent leaving OC1 code field if invalid
oc1CodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('OC')) {
oc1ErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OC');
// Return focus to this field
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
// Prevent Tab/Enter from moving to next field if OC1 code is invalid
oc1CodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
e.preventDefault();
oc1ErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OC');
this.select();
}
});
// Prevent focusing on OC2 code if CP code is invalid
oc2CodeInput.addEventListener('focus', function(e) {
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
cpErrorMessage.classList.add('show');
cpCodeInput.focus();
cpCodeInput.select();
}
// Also check if OC1 is invalid
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
e.preventDefault();
oc1ErrorMessage.classList.add('show');
oc1CodeInput.focus();
oc1CodeInput.select();
}
});
// Validate OC2 code on input
oc2CodeInput.addEventListener('input', function() {
const value = this.value.toUpperCase();
this.value = value; // Convert to uppercase
if (value.length >= 2 && !value.startsWith('OC')) {
oc2ErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OC');
} else {
oc2ErrorMessage.classList.remove('show');
this.setCustomValidity('');
// Auto-advance when field is complete and valid
if (value.length === 4 && value.startsWith('OC')) {
defectCodeInput.focus();
}
}
});
// Prevent leaving OC2 code field if invalid
oc2CodeInput.addEventListener('blur', function(e) {
if (this.value.length > 0 && !this.value.startsWith('OC')) {
oc2ErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OC');
// Return focus to this field
setTimeout(() => {
this.focus();
this.select();
}, 0);
}
});
// Prevent Tab/Enter from moving to next field if OC2 code is invalid
oc2CodeInput.addEventListener('keydown', function(e) {
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
e.preventDefault();
oc2ErrorMessage.classList.add('show');
this.setCustomValidity('Must start with OC');
this.select();
}
});
// Prevent focusing on defect code if CP code is invalid
defectCodeInput.addEventListener('focus', function(e) {
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
cpErrorMessage.classList.add('show');
cpCodeInput.focus();
cpCodeInput.select();
}
// Also check if OC1 is invalid
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
e.preventDefault();
oc1ErrorMessage.classList.add('show');
oc1CodeInput.focus();
oc1CodeInput.select();
}
// Also check if OC2 is invalid
if (oc2CodeInput.value.length > 0 && !oc2CodeInput.value.startsWith('OC')) {
e.preventDefault();
oc2ErrorMessage.classList.add('show');
oc2CodeInput.focus();
oc2CodeInput.select();
}
});
// Validate defect code on input - only allow digits
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) {
defectErrorMessage.classList.add('show');
this.setCustomValidity('Must be a 3-digit number');
} else {
defectErrorMessage.classList.remove('show');
this.setCustomValidity('');
}
} else {
defectErrorMessage.classList.remove('show');
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.value.startsWith('OP')) {
operatorErrorMessage.classList.add('show');
operatorCodeInput.focus();
operatorCodeInput.setCustomValidity('Must start with OP');
return;
}
// Validate CP code before submitting
if (!cpCodeInput.value.startsWith('CP')) {
cpErrorMessage.classList.add('show');
cpCodeInput.focus();
cpCodeInput.setCustomValidity('Must start with CP');
return;
}
// Validate OC1 code before submitting
if (!oc1CodeInput.value.startsWith('OC')) {
oc1ErrorMessage.classList.add('show');
oc1CodeInput.focus();
oc1CodeInput.setCustomValidity('Must start with OC');
return;
}
// Validate OC2 code before submitting
if (!oc2CodeInput.value.startsWith('OC')) {
oc2ErrorMessage.classList.add('show');
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) {
defectErrorMessage.classList.add('show');
this.focus();
this.setCustomValidity('Must be a 3-digit number');
return;
}
// Clear all custom validity states before submitting
operatorCodeInput.setCustomValidity('');
cpCodeInput.setCustomValidity('');
oc1CodeInput.setCustomValidity('');
oc2CodeInput.setCustomValidity('');
this.setCustomValidity('');
// Update time field before submitting
const timeInput = document.getElementById('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');
timeInput.value = `${hours}:${minutes}:${seconds}`;
// Save current CP code and defect code 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);
// Check if scan-to-boxes is enabled and defect code is 000
console.log('Auto-submit check:', {
scanToBoxesEnabled: scanToBoxesEnabled,
defectCode: this.value,
shouldShowModal: scanToBoxesEnabled && this.value === '000'
});
if (scanToBoxesEnabled && this.value === '000') {
console.log('Auto-submit: Scan-to-boxes enabled, calling submitScanWithBoxAssignment');
submitScanWithBoxAssignment();
} else {
console.log('Auto-submit: Normal form submission - setting flag and submitting');
isAutoSubmitting = true;
// Submit the form normally
console.log('Calling form.submit() - form:', form);
form.submit();
console.log('form.submit() called successfully');
}
}
});
// Validate form on submit
form.addEventListener('submit', async function(e) {
// Skip validation if this is an auto-submit (already validated)
if (isAutoSubmitting) {
console.log('Auto-submit in progress, skipping duplicate validation');
isAutoSubmitting = false; // Reset flag
return true; // Allow submission to proceed
}
console.log('Manual form submission, running validation');
let hasError = false;
if (!operatorCodeInput.value.startsWith('OP')) {
e.preventDefault();
operatorErrorMessage.classList.add('show');
operatorCodeInput.setCustomValidity('Must start with OP');
if (!hasError) {
operatorCodeInput.focus();
hasError = true;
}
}
if (!cpCodeInput.value.startsWith('CP')) {
e.preventDefault();
cpErrorMessage.classList.add('show');
cpCodeInput.setCustomValidity('Must start with CP');
if (!hasError) {
cpCodeInput.focus();
hasError = true;
}
}
if (!oc1CodeInput.value.startsWith('OC')) {
e.preventDefault();
oc1ErrorMessage.classList.add('show');
oc1CodeInput.setCustomValidity('Must start with OC');
if (!hasError) {
oc1CodeInput.focus();
hasError = true;
}
}
if (!oc2CodeInput.value.startsWith('OC')) {
e.preventDefault();
oc2ErrorMessage.classList.add('show');
oc2CodeInput.setCustomValidity('Must start with OC');
if (!hasError) {
oc2CodeInput.focus();
hasError = true;
}
}
// Validate defect code is a 3-digit number
const isValidDefectCode = /^\d{3}$/.test(defectCodeInput.value);
if (!isValidDefectCode) {
e.preventDefault();
defectErrorMessage.classList.add('show');
defectCodeInput.setCustomValidity('Must be a 3-digit number');
if (!hasError) {
defectCodeInput.focus();
hasError = true;
}
}
// If validation passed and scan-to-boxes is enabled, intercept submission
if (!hasError && scanToBoxesEnabled) {
console.log('Validation passed, intercepting for scan-to-boxes');
e.preventDefault();
e.stopPropagation();
await submitScanWithBoxAssignment();
return false;
}
});
// Add functionality for clear saved codes button
const clearSavedBtn = document.getElementById('clear-saved-btn');
clearSavedBtn.addEventListener('click', function() {
if (confirm('Clear saved Quality Operator code? You will need to re-enter it.')) {
// Clear localStorage (only Quality Operator Code)
localStorage.removeItem('fg_scan_operator_code');
localStorage.removeItem('fg_scan_clear_after_submit');
localStorage.removeItem('fg_scan_last_cp');
localStorage.removeItem('fg_scan_last_defect');
// Clear Quality Operator Code field only
operatorCodeInput.value = '';
// Focus on operator code field
operatorCodeInput.focus();
// Show confirmation
alert('✅ Saved Quality Operator code cleared! Please re-enter your operator code.');
}
});
// Initialize QZ Tray for printing box labels - only if scan-to-boxes is enabled
function initializeQzTray() {
if (window.qz && scanToBoxesEnabled) {
window.qz.websocket.connect().then(() => {
console.log('QZ Tray connected for box label printing');
}).catch(err => {
console.warn('QZ Tray not available:', err);
});
} else if (window.qz && !scanToBoxesEnabled) {
console.log('Scan-to-boxes disabled, skipping QZ Tray connection');
}
}
// Initialize on page load if enabled
if (scanToBoxesEnabled) {
initializeQzTray();
}
});
</script>
<script>
// Functions for scan-to-boxes feature
document.addEventListener('DOMContentLoaded', function() {
// Quick box creation button
document.getElementById('quick-box-create-btn').addEventListener('click', async function() {
// Check if scan-to-boxes is enabled
if (!scanToBoxesEnabled) {
showNotification('⚠️ Please enable "Scan to Boxes" feature first', 'warning');
return;
}
try {
this.disabled = true;
this.textContent = 'Creating...';
// Step 1: Create box in database
const createResponse = await fetch('/warehouse/manage_boxes', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'action=add_box'
});
if (!createResponse.ok) throw new Error('Failed to create box');
const result = await createResponse.json();
if (result.success && result.box_number) {
const boxNumber = result.box_number;
// Step 2: Generate PDF label and print using QZ Tray
try {
// Check QZ Tray connection
if (!window.qz || !window.qz.websocket.isActive()) {
console.log('QZ Tray not connected, attempting to connect...');
await window.qz.websocket.connect();
}
// Get available printers
const printers = await window.qz.printers.find();
if (printers.length === 0) {
throw new Error('No printers found');
}
// Try to get default printer, fallback to first available
let printer;
try {
printer = await window.qz.printers.getDefault();
console.log('Using default printer:', printer);
} catch (e) {
printer = printers[0];
console.log('Default printer not found, using first available:', printer);
}
// Generate PDF from backend
const formData = new FormData();
formData.append('box_number', boxNumber);
const pdfResponse = await fetch('/generate_box_label_pdf', {
method: 'POST',
body: formData
});
if (!pdfResponse.ok) {
throw new Error('Failed to generate PDF label');
}
// Get PDF as blob and convert to base64
const pdfBlob = await pdfResponse.blob();
const pdfBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(pdfBlob);
});
console.log('PDF generated, sending to printer...');
// Configure QZ Tray for PDF printing
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 }
});
const printData = [{
type: 'pdf',
format: 'base64',
data: pdfBase64
}];
await window.qz.print(config, printData);
showNotification(`✅ Box ${boxNumber} created and label printed!`, 'success');
// Step 3: Keep modal open and focus on scan input
document.getElementById('scan-box-input').value = '';
document.getElementById('scan-box-input').focus();
document.getElementById('scan-box-input').placeholder = 'Scan the printed label now...';
} catch (printError) {
console.error('Print error:', printError);
showNotification(`⚠️ Box ${boxNumber} created but print failed: ${printError.message}\n\nPlease ensure QZ Tray is running and a printer is available.`, 'warning');
// Still keep modal open for manual entry
document.getElementById('scan-box-input').value = boxNumber;
document.getElementById('scan-box-input').focus();
}
} else {
throw new Error(result.error || 'Failed to create box');
}
} catch (error) {
console.error('Error creating box:', error);
showNotification('❌ Error creating box: ' + error.message, 'error');
} finally {
// Re-enable button
this.disabled = false;
this.textContent = '📦 Quick Box Label Creation';
}
});
// Assign to scanned box button
document.getElementById('assign-to-box-btn').addEventListener('click', async function() {
// Check if scan-to-boxes is enabled
if (!scanToBoxesEnabled) {
showNotification('⚠️ "Scan to Boxes" feature is disabled', 'warning');
closeBoxModal();
return;
}
const boxNumber = document.getElementById('scan-box-input').value.trim();
if (!boxNumber) {
showNotification('⚠️ Please scan or enter a box number', 'warning');
return;
}
try {
await assignCpToBox(boxNumber);
showNotification(`✅ CP ${currentCpCode} assigned to box ${boxNumber}`, 'success');
setTimeout(() => closeBoxModal(), 1000);
} catch (error) {
showNotification('❌ Error: ' + error.message, 'error');
}
});
});
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('box-assignment-modal');
if (event.target == modal) {
closeBoxModal();
}
}
</script>
{% endblock %}
{% block content %}
<!-- Floating Help Button -->
<div class="floating-help-btn">
<a href="{{ url_for('main.help', page='fg_scan') }}" target="_blank" title="FG Scan Help">
📖
</a>
</div>
<div class="scan-container">
<!-- Input Form Card -->
<div class="card scan-form-card">
<h3>Scan Input</h3>
<form method="POST" class="form-centered" id="fg-scan-form">
<label for="operator_code">Quality Operator Code:</label>
<input type="text" id="operator_code" name="operator_code" maxlength="4" required>
<label for="cp_code">CP Code:</label>
<input type="text" id="cp_code" name="cp_code" maxlength="15" required>
<label for="oc1_code">OC1 Code:</label>
<input type="text" id="oc1_code" name="oc1_code" maxlength="4" required>
<label for="oc2_code">OC2 Code:</label>
<input type="text" id="oc2_code" name="oc2_code" maxlength="4" required>
<label for="defect_code">Defect Code:</label>
<input type="text" id="defect_code" name="defect_code" maxlength="4" required>
<label for="date">Date:</label>
<input type="text" id="date" name="date" value="{{ now().strftime('%Y-%m-%d') }}" placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" required>
<label for="time">Time:</label>
<input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly>
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn" id="clear-saved-btn" style="background-color: #ff6b6b; margin-left: 10px;">Clear Quality Operator</button>
<!-- Enable/Disable Scan to Boxes Toggle -->
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
<label style="display: flex; align-items: center; justify-content: center; gap: 10px; cursor: pointer;">
<span style="font-weight: bold;">Enable Scan to Boxes:</span>
<input type="checkbox" id="scan-to-boxes-toggle" style="width: 20px; height: 20px; cursor: pointer;">
</label>
<p style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
When enabled, good quality scans (000) will prompt for box assignment
</p>
</div>
</form>
</div>
<!-- Latest Scans Card -->
<div class="card scan-table-card">
<h3>Latest Scans</h3>
<div class="report-table-container">
<table class="scan-table">
<thead>
<tr>
<th>ID</th>
<th>Op Code</th>
<th>CP Code</th>
<th>OC1 Code</th>
<th>OC2 Code</th>
<th>Defect Code</th>
<th>Date</th>
<th>Time</th>
<th>Apr. Quantity</th>
<th>Rejec. Quantity</th>
</tr>
</thead>
<tbody>
{% for row in scan_data %}
<tr>
<td>{{ row[0] }}</td> <!-- Id -->
<td>{{ row[1] }}</td> <!-- operator_code -->
<td>{{ row[2] }}</td> <!-- CP_full_code -->
<td>{{ row[3] }}</td> <!-- OC1_code -->
<td>{{ row[4] }}</td> <!-- OC2_code -->
<td>{{ row[5] }}</td> <!-- quality_code -->
<td>{{ row[6] }}</td> <!-- date -->
<td>{{ row[7] }}</td> <!-- time -->
<td>{{ row[8] }}</td> <!-- approved quantity -->
<td>{{ row[9] }}</td> <!-- rejected quantity -->
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Box Assignment Popup Modal -->
<div id="box-assignment-modal" class="box-modal" style="display: none;">
<div class="box-modal-content">
<div class="box-modal-header">
<h3>Assign to Box</h3>
<span class="box-modal-close" onclick="closeBoxModal()">&times;</span>
</div>
<div class="box-modal-body">
<p>CP Code: <strong id="modal-cp-code"></strong></p>
<!-- Quick Box Creation -->
<div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border-radius: 5px;">
<button type="button" id="quick-box-create-btn" class="btn" style="width: 100%; background: #28a745; color: white;">
📦 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>
<div style="text-align: center; margin: 15px 0; color: #999;">— OR —</div>
<!-- Scan Existing Box -->
<div style="margin: 20px 0;">
<label style="font-weight: bold;">Scan Box Number:</label>
<input type="text" id="scan-box-input" placeholder="Scan or enter box number" style="width: 100%; padding: 8px; font-size: 1em; margin-top: 5px;">
<p style="font-size: 0.85em; color: #666; margin-top: 5px;">
Scan an existing box label to assign this CP code to that box
</p>
</div>
<div class="box-modal-buttons" style="margin-top: 20px;">
<button type="button" class="btn" onclick="closeBoxModal()" style="background: #6c757d;">Skip</button>
<button type="button" id="assign-to-box-btn" class="btn" style="background: #007bff;">Assign to Box</button>
</div>
</div>
</div>
</div>
<style>
.box-modal {
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.box-modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 0;
border: 1px solid #888;
width: 500px;
max-width: 90%;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.box-modal-header {
padding: 15px 20px;
background-color: #007bff;
color: white;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.box-modal-header h3 {
margin: 0;
}
.box-modal-close {
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 20px;
}
.box-modal-close:hover {
color: #ccc;
}
.box-modal-body {
padding: 20px;
}
.box-modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
body.dark-mode .box-modal-content {
background-color: #1e293b;
border: 1px solid #334155;
}
body.dark-mode .box-modal-body {
color: #e2e8f0;
}
body.dark-mode #scan-box-input {
background: #0f172a;
border: 1px solid #334155;
color: #e2e8f0;
}
</style>
{% endblock %}