- Add SchemaVerifier class for automatic database schema verification and repair - Implement warehouse_manager (Level 75) and warehouse_worker (Level 35) roles - Add zone-based access control for warehouse workers - Implement worker-manager binding system with zone filtering - Add comprehensive database auto-repair on Docker initialization - Remove Module Access section from user form (role-based access only) - Add autocomplete attributes to password fields for better UX - Include detailed documentation for warehouse implementation - Update initialize_db.py with schema verification as Step 0
1005 lines
36 KiB
HTML
1005 lines
36 KiB
HTML
{% 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>
|
||
<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>
|
||
<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">
|
||
<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">
|
||
<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">
|
||
<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>
|
||
|
||
<div id="quickBoxSection" style="display: none;" class="quick-box-section">
|
||
<button type="button" class="btn-secondary" id="quickBoxLabel">Quick Box Label Creation</button>
|
||
</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">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<label for="boxNumber">Box Number:</label>
|
||
<input type="text" id="boxNumber" placeholder="Enter box number">
|
||
<label for="boxQty">Quantity:</label>
|
||
<input type="number" id="boxQty" placeholder="Enter quantity" min="1">
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn-secondary" id="cancelModal">Cancel</button>
|
||
<button type="button" class="btn-submit" id="assignToBox">Assign</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Global variables
|
||
let scanToBoxesEnabled = false;
|
||
let currentCpCode = '';
|
||
let qzTrayReady = false;
|
||
let cpCodeLastInputTime = null;
|
||
|
||
// 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');
|
||
|
||
// 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
|
||
const operatorErrorMessage = document.createElement('div');
|
||
operatorErrorMessage.className = 'error-message';
|
||
operatorErrorMessage.id = 'operator-error';
|
||
operatorErrorMessage.textContent = 'Operator code must start with OP and be 4 characters';
|
||
operatorCodeInput.parentNode.insertBefore(operatorErrorMessage, operatorCodeInput.nextSibling);
|
||
|
||
const cpErrorMessage = document.createElement('div');
|
||
cpErrorMessage.className = 'error-message';
|
||
cpErrorMessage.id = 'cp-error';
|
||
cpErrorMessage.textContent = 'CP code must start with CP and be 15 characters';
|
||
cpCodeInput.parentNode.insertBefore(cpErrorMessage, cpCodeInput.nextSibling);
|
||
|
||
const oc1ErrorMessage = document.createElement('div');
|
||
oc1ErrorMessage.className = 'error-message';
|
||
oc1ErrorMessage.id = 'oc1-error';
|
||
oc1ErrorMessage.textContent = 'OC1 code must start with OC and be 4 characters';
|
||
oc1CodeInput.parentNode.insertBefore(oc1ErrorMessage, oc1CodeInput.nextSibling);
|
||
|
||
const oc2ErrorMessage = document.createElement('div');
|
||
oc2ErrorMessage.className = 'error-message';
|
||
oc2ErrorMessage.id = 'oc2-error';
|
||
oc2ErrorMessage.textContent = 'OC2 code must start with OC and be 4 characters';
|
||
oc2CodeInput.parentNode.insertBefore(oc2ErrorMessage, oc2CodeInput.nextSibling);
|
||
|
||
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);
|
||
|
||
// ===== 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)');
|
||
}
|
||
}
|
||
}
|
||
|
||
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 prefix
|
||
if (currentValue.length >= 2 && !currentValue.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 (currentValue.length === 15 && currentValue.startsWith('CP')) {
|
||
setTimeout(() => {
|
||
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
|
||
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)
|
||
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
|
||
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();
|
||
}
|
||
});
|
||
|
||
// ===== OPERATOR CODE VALIDATION =====
|
||
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')) {
|
||
setTimeout(() => {
|
||
cpCodeInput.focus();
|
||
console.log('✅ Auto-advanced from Operator Code to CP Code');
|
||
}, 50);
|
||
}
|
||
}
|
||
});
|
||
|
||
operatorCodeInput.addEventListener('blur', function(e) {
|
||
if (this.value.length > 0 && !this.value.startsWith('OP')) {
|
||
operatorErrorMessage.classList.add('show');
|
||
this.setCustomValidity('Must start with OP');
|
||
setTimeout(() => {
|
||
this.focus();
|
||
this.select();
|
||
}, 0);
|
||
}
|
||
});
|
||
|
||
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();
|
||
}
|
||
});
|
||
|
||
// ===== OC1 CODE VALIDATION =====
|
||
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')) {
|
||
setTimeout(() => {
|
||
oc2CodeInput.focus();
|
||
console.log('✅ Auto-advanced from OC1 Code to OC2 Code');
|
||
}, 50);
|
||
}
|
||
}
|
||
});
|
||
|
||
oc1CodeInput.addEventListener('blur', function(e) {
|
||
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||
oc1ErrorMessage.classList.add('show');
|
||
this.setCustomValidity('Must start with OC');
|
||
setTimeout(() => {
|
||
this.focus();
|
||
this.select();
|
||
}, 0);
|
||
}
|
||
});
|
||
|
||
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();
|
||
}
|
||
});
|
||
|
||
oc1CodeInput.addEventListener('focus', function(e) {
|
||
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||
e.preventDefault();
|
||
cpErrorMessage.classList.add('show');
|
||
cpCodeInput.focus();
|
||
cpCodeInput.select();
|
||
}
|
||
});
|
||
|
||
// ===== OC2 CODE VALIDATION =====
|
||
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')) {
|
||
setTimeout(() => {
|
||
defectCodeInput.focus();
|
||
console.log('✅ Auto-advanced from OC2 Code to Defect Code');
|
||
}, 50);
|
||
}
|
||
}
|
||
});
|
||
|
||
oc2CodeInput.addEventListener('blur', function(e) {
|
||
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||
oc2ErrorMessage.classList.add('show');
|
||
this.setCustomValidity('Must start with OC');
|
||
setTimeout(() => {
|
||
this.focus();
|
||
this.select();
|
||
}, 0);
|
||
}
|
||
});
|
||
|
||
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();
|
||
}
|
||
});
|
||
|
||
oc2CodeInput.addEventListener('focus', function(e) {
|
||
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||
e.preventDefault();
|
||
cpErrorMessage.classList.add('show');
|
||
cpCodeInput.focus();
|
||
cpCodeInput.select();
|
||
}
|
||
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||
e.preventDefault();
|
||
oc1ErrorMessage.classList.add('show');
|
||
oc1CodeInput.focus();
|
||
oc1CodeInput.select();
|
||
}
|
||
});
|
||
|
||
// ===== DEFECT CODE VALIDATION =====
|
||
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') || cpCodeInput.value.length !== 15) {
|
||
cpErrorMessage.classList.add('show');
|
||
cpCodeInput.focus();
|
||
cpCodeInput.setCustomValidity('Must start with CP and be complete');
|
||
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('');
|
||
|
||
// ===== 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
|
||
console.log('Auto-submitting form on 3-digit defect code');
|
||
document.getElementById('scanForm').submit();
|
||
}
|
||
});
|
||
|
||
defectCodeInput.addEventListener('focus', function(e) {
|
||
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||
e.preventDefault();
|
||
cpErrorMessage.classList.add('show');
|
||
cpCodeInput.focus();
|
||
cpCodeInput.select();
|
||
}
|
||
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||
e.preventDefault();
|
||
oc1ErrorMessage.classList.add('show');
|
||
oc1CodeInput.focus();
|
||
oc1CodeInput.select();
|
||
}
|
||
if (oc2CodeInput.value.length > 0 && !oc2CodeInput.value.startsWith('OC')) {
|
||
e.preventDefault();
|
||
oc2ErrorMessage.classList.add('show');
|
||
oc2CodeInput.focus();
|
||
oc2CodeInput.select();
|
||
}
|
||
});
|
||
|
||
// ===== CLEAR OPERATOR CODE BUTTON =====
|
||
document.getElementById('clearOperator').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 =====
|
||
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');
|
||
resetForm();
|
||
document.getElementById('boxAssignmentModal').style.display = 'flex';
|
||
} else {
|
||
showNotification('Error saving scan', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('Error saving scan', 'error');
|
||
});
|
||
} else {
|
||
// Regular form submission
|
||
this.submit();
|
||
}
|
||
});
|
||
|
||
// Scan to boxes functionality
|
||
document.getElementById('scanToBoxes').addEventListener('change', function() {
|
||
scanToBoxesEnabled = 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();
|
||
}
|
||
});
|
||
|
||
// Quick box label creation
|
||
document.getElementById('quickBoxLabel').addEventListener('click', function() {
|
||
if (!qzTrayReady) {
|
||
alert('QZ Tray is not connected. Please ensure QZ Tray is running.');
|
||
return;
|
||
}
|
||
|
||
const cpCode = document.getElementById('cp_code').value.trim();
|
||
if (!cpCode) {
|
||
alert('Please enter a CP code first');
|
||
return;
|
||
}
|
||
|
||
// Create label configuration for QZ Tray
|
||
const label = {
|
||
type: 'label',
|
||
cpCode: cpCode,
|
||
createdAt: new Date().toISOString()
|
||
};
|
||
|
||
// Send to printer via QZ Tray
|
||
qz.print({
|
||
type: 'label',
|
||
format: cpCode
|
||
}).catch(function(err) {
|
||
console.error('Print error:', err);
|
||
alert('Error printing label');
|
||
});
|
||
});
|
||
|
||
// Modal functionality
|
||
document.getElementById('closeModal').addEventListener('click', function() {
|
||
document.getElementById('boxAssignmentModal').style.display = 'none';
|
||
});
|
||
|
||
document.getElementById('cancelModal').addEventListener('click', function() {
|
||
document.getElementById('boxAssignmentModal').style.display = 'none';
|
||
});
|
||
|
||
document.getElementById('assignToBox').addEventListener('click', function() {
|
||
const boxNumber = document.getElementById('boxNumber').value.trim();
|
||
const boxQty = document.getElementById('boxQty').value.trim();
|
||
|
||
if (!boxNumber) {
|
||
alert('Please enter a box number');
|
||
return;
|
||
}
|
||
|
||
if (!boxQty || isNaN(boxQty) || parseInt(boxQty) < 1) {
|
||
alert('Please enter a valid quantity');
|
||
return;
|
||
}
|
||
|
||
// Submit box assignment
|
||
const data = {
|
||
box_number: boxNumber,
|
||
quantity: boxQty,
|
||
cp_code: currentCpCode
|
||
};
|
||
|
||
fetch('{{ url_for("quality.fg_scan") }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showNotification('Box assigned successfully!', 'success');
|
||
document.getElementById('boxAssignmentModal').style.display = 'none';
|
||
document.getElementById('boxNumber').value = '';
|
||
document.getElementById('boxQty').value = '';
|
||
}
|
||
})
|
||
.catch(error => console.error('Error:', error));
|
||
});
|
||
|
||
// 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 %}
|