1187 lines
46 KiB
HTML
1187 lines
46 KiB
HTML
{% 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() {
|
||
// Load toggle state FIRST
|
||
const savedState = localStorage.getItem('scan_to_boxes_enabled');
|
||
if (savedState === 'true') {
|
||
scanToBoxesEnabled = true;
|
||
}
|
||
console.log('Initial scanToBoxesEnabled:', scanToBoxesEnabled);
|
||
|
||
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;
|
||
}
|
||
|
||
// 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');
|
||
// Submit the form normally
|
||
form.submit();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Validate form on submit
|
||
form.addEventListener('submit', async function(e) {
|
||
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()">×</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 %}
|