feat: Add Set Orders on Boxes feature with debouncing and page refresh
- Created warehouse_orders.py module with 8 order management functions - Added /warehouse/set-orders-on-boxes route and 7 API endpoints - Implemented 4-tab interface: assign, find, move, and view orders - Changed assign input from dropdown to text field with BOX validation - Fixed location join issue in warehouse.py (use boxes_crates.location_id) - Added debouncing flag to prevent multiple rapid form submissions - Added page refresh after successful order assignment - Disabled assign button during processing - Added page refresh with 2 second delay for UX feedback - Added CP code validation in inventory page - Improved modal styling with theme support - Fixed set_boxes_locations page to refresh box info after assignments
This commit is contained in:
@@ -40,8 +40,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">
|
||||
Searches for any CP code starting with the entered text. Shows all related entries.
|
||||
<i class="fas fa-info-circle"></i> Must start with "CP" (e.g., CP00000001). Searches for any CP code starting with the entered text.
|
||||
</small>
|
||||
<div id="cpCodeValidation" class="alert alert-warning d-none mt-2 mb-0 py-2" role="alert">
|
||||
<small><i class="fas fa-exclamation-triangle"></i> <span id="cpCodeValidationText"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,14 +136,14 @@
|
||||
</div>
|
||||
|
||||
<!-- CP Details Modal -->
|
||||
<div class="modal fade" id="cpDetailsModal" tabindex="-1">
|
||||
<div class="modal fade" id="cpDetailsModal" tabindex="-1" aria-labelledby="cpDetailsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">
|
||||
<h5 class="modal-title" id="cpDetailsModalLabel">
|
||||
<i class="fas fa-details"></i> CP Code Details
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="cpDetailsContent">
|
||||
@@ -189,6 +192,75 @@
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Modal Theme Styling */
|
||||
.modal-content {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-color);
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-header .modal-title {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: var(--bg-secondary);
|
||||
border-top-color: var(--border-color);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Modal Button Close - Theme aware */
|
||||
.btn-close {
|
||||
filter: invert(0);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-close {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* Modal Table Styling */
|
||||
.modal-body table {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.modal-body th {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-color);
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-body td {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-color);
|
||||
transition: color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Override hardcoded Bootstrap classes in modal */
|
||||
.modal-header.bg-primary {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.modal-header.text-white {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -197,44 +269,169 @@ let inventoryData = [];
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Ensure input fields are enabled and ready for input
|
||||
const cpCodeSearchEl = document.getElementById('cpCodeSearch');
|
||||
const boxNumberSearchEl = document.getElementById('boxNumberSearch');
|
||||
|
||||
if (cpCodeSearchEl) {
|
||||
cpCodeSearchEl.disabled = false;
|
||||
cpCodeSearchEl.readOnly = false;
|
||||
cpCodeSearchEl.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') searchByCpCode();
|
||||
});
|
||||
// Add real-time validation for CP code
|
||||
cpCodeSearchEl.addEventListener('input', function(e) {
|
||||
validateCpCodeInput(this.value);
|
||||
});
|
||||
cpCodeSearchEl.addEventListener('blur', function(e) {
|
||||
validateCpCodeInput(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (boxNumberSearchEl) {
|
||||
boxNumberSearchEl.disabled = false;
|
||||
boxNumberSearchEl.readOnly = false;
|
||||
boxNumberSearchEl.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') searchByBoxNumber();
|
||||
});
|
||||
}
|
||||
|
||||
// Load inventory after setting up input fields
|
||||
loadInventory();
|
||||
|
||||
// Auto-search on Enter key
|
||||
document.getElementById('cpCodeSearch').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') searchByCpCode();
|
||||
});
|
||||
|
||||
document.getElementById('boxNumberSearch').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') searchByBoxNumber();
|
||||
});
|
||||
});
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
const alert = document.getElementById('statusAlert');
|
||||
const messageEl = document.getElementById('statusMessage');
|
||||
messageEl.textContent = message;
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.classList.remove('d-none');
|
||||
// Validate CP Code input - must start with "CP"
|
||||
function validateCpCodeInput(value) {
|
||||
const validationDiv = document.getElementById('cpCodeValidation');
|
||||
const validationText = document.getElementById('cpCodeValidationText');
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
alert.classList.add('d-none');
|
||||
}, 5000);
|
||||
if (!validationDiv || !validationText) return;
|
||||
|
||||
// If field is empty, hide validation message
|
||||
if (!value || value.trim() === '') {
|
||||
validationDiv.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if starts with "CP"
|
||||
if (!value.toUpperCase().startsWith('CP')) {
|
||||
validationText.textContent = 'CP code must start with "CP" (e.g., CP00000001)';
|
||||
validationDiv.classList.remove('d-none');
|
||||
} else {
|
||||
validationDiv.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
// Try to find the alert elements
|
||||
let alert = document.getElementById('statusAlert');
|
||||
let messageEl = document.getElementById('statusMessage');
|
||||
|
||||
// If elements don't exist, create them
|
||||
if (!alert) {
|
||||
const container = document.querySelector('.container-fluid') || document.body;
|
||||
alert = document.createElement('div');
|
||||
alert.id = 'statusAlert';
|
||||
alert.className = `alert alert-${type} d-none`;
|
||||
alert.setAttribute('role', 'alert');
|
||||
|
||||
messageEl = document.createElement('span');
|
||||
messageEl.id = 'statusMessage';
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'fas fa-info-circle';
|
||||
|
||||
alert.appendChild(icon);
|
||||
alert.appendChild(document.createTextNode(' '));
|
||||
alert.appendChild(messageEl);
|
||||
|
||||
// Insert after first container-fluid div
|
||||
if (container && container.firstChild) {
|
||||
container.insertBefore(alert, container.firstChild.nextSibling);
|
||||
} else {
|
||||
document.body.insertBefore(alert, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Update message element if it was just created or found
|
||||
if (messageEl) {
|
||||
messageEl.textContent = message;
|
||||
}
|
||||
|
||||
if (alert) {
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.classList.remove('d-none');
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
alert.classList.add('d-none');
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('loadingSpinner').classList.remove('d-none');
|
||||
let spinner = document.getElementById('loadingSpinner');
|
||||
if (!spinner) {
|
||||
spinner = document.createElement('div');
|
||||
spinner.id = 'loadingSpinner';
|
||||
spinner.className = 'spinner-border d-none';
|
||||
spinner.setAttribute('role', 'status');
|
||||
spinner.style.display = 'none';
|
||||
spinner.innerHTML = '<span class="sr-only">Loading...</span>';
|
||||
document.body.appendChild(spinner);
|
||||
}
|
||||
spinner.classList.remove('d-none');
|
||||
spinner.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loadingSpinner').classList.add('d-none');
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
if (spinner) {
|
||||
spinner.classList.add('d-none');
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to safely get and update element text content
|
||||
function safeSetElementText(elementId, text) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (el) {
|
||||
el.textContent = text;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
// Helper function to safely get element value
|
||||
function safeGetElementValue(elementId) {
|
||||
const el = document.getElementById(elementId);
|
||||
return el ? (el.value || '') : '';
|
||||
}
|
||||
|
||||
// Helper function to safely set element value
|
||||
function safeSetElementValue(elementId, value) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (el) {
|
||||
el.value = value;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function loadInventory() {
|
||||
showLoading();
|
||||
currentSearchType = 'all';
|
||||
document.getElementById('cpCodeSearch').value = '';
|
||||
document.getElementById('boxNumberSearch').value = '';
|
||||
|
||||
// Clear search fields but ensure they're enabled
|
||||
const cpField = safeSetElementValue('cpCodeSearch', '');
|
||||
const boxField = safeSetElementValue('boxNumberSearch', '');
|
||||
|
||||
// Ensure fields are not disabled
|
||||
if (cpField) {
|
||||
cpField.disabled = false;
|
||||
}
|
||||
if (boxField) {
|
||||
boxField.disabled = false;
|
||||
}
|
||||
|
||||
fetch('/warehouse/api/cp-inventory?limit=500&offset=0')
|
||||
.then(response => {
|
||||
@@ -245,9 +442,9 @@ function loadInventory() {
|
||||
if (data.success) {
|
||||
inventoryData = data.inventory;
|
||||
renderTable(data.inventory);
|
||||
document.getElementById('totalRecords').textContent = data.count;
|
||||
document.getElementById('showingRecords').textContent = data.count;
|
||||
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
|
||||
safeSetElementText('totalRecords', data.count);
|
||||
safeSetElementText('showingRecords', data.count);
|
||||
safeSetElementText('lastUpdated', new Date().toLocaleTimeString());
|
||||
showStatus(`Loaded ${data.count} inventory items`, 'success');
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
@@ -265,13 +462,19 @@ function reloadInventory() {
|
||||
}
|
||||
|
||||
function searchByCpCode() {
|
||||
const cpCode = document.getElementById('cpCodeSearch').value.trim();
|
||||
const cpCode = safeGetElementValue('cpCodeSearch').trim();
|
||||
|
||||
if (!cpCode) {
|
||||
showStatus('Please enter a CP code', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that CP code starts with "CP"
|
||||
if (!cpCode.toUpperCase().startsWith('CP')) {
|
||||
showStatus('CP code must start with "CP" (e.g., CP00000001)', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
currentSearchType = 'cp';
|
||||
|
||||
@@ -287,8 +490,8 @@ function searchByCpCode() {
|
||||
if (data.success) {
|
||||
inventoryData = data.results;
|
||||
renderTable(data.results);
|
||||
document.getElementById('totalRecords').textContent = data.count;
|
||||
document.getElementById('showingRecords').textContent = data.count;
|
||||
safeSetElementText('totalRecords', data.count);
|
||||
safeSetElementText('showingRecords', data.count);
|
||||
showStatus(`Found ${data.count} entries for CP code: ${cpCode}`, 'success');
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
@@ -302,12 +505,12 @@ function searchByCpCode() {
|
||||
}
|
||||
|
||||
function clearCpSearch() {
|
||||
document.getElementById('cpCodeSearch').value = '';
|
||||
safeSetElementValue('cpCodeSearch', '');
|
||||
loadInventory();
|
||||
}
|
||||
|
||||
function searchByBoxNumber() {
|
||||
const boxNumber = document.getElementById('boxNumberSearch').value.trim();
|
||||
const boxNumber = safeGetElementValue('boxNumberSearch').trim();
|
||||
|
||||
if (!boxNumber) {
|
||||
showStatus('Please enter a box number', 'warning');
|
||||
@@ -329,8 +532,8 @@ function searchByBoxNumber() {
|
||||
if (data.success) {
|
||||
inventoryData = data.results;
|
||||
renderBoxSearchTable(data.results);
|
||||
document.getElementById('totalRecords').textContent = data.count;
|
||||
document.getElementById('showingRecords').textContent = data.count;
|
||||
safeSetElementText('totalRecords', data.count);
|
||||
safeSetElementText('showingRecords', data.count);
|
||||
showStatus(`Found ${data.count} CP entries in box: ${boxNumber}`, 'success');
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
@@ -344,13 +547,18 @@ function searchByBoxNumber() {
|
||||
}
|
||||
|
||||
function clearBoxSearch() {
|
||||
document.getElementById('boxNumberSearch').value = '';
|
||||
safeSetElementValue('boxNumberSearch', '');
|
||||
loadInventory();
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
|
||||
if (!tbody) {
|
||||
console.warn('tableBody element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No inventory records found</td></tr>';
|
||||
return;
|
||||
@@ -413,6 +621,11 @@ function renderTable(data) {
|
||||
function renderBoxSearchTable(data) {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
|
||||
if (!tbody) {
|
||||
console.warn('tableBody element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No CP entries found in this box</td></tr>';
|
||||
return;
|
||||
@@ -492,6 +705,12 @@ function viewCpDetails(cpCode) {
|
||||
const modal = document.getElementById('cpDetailsModal');
|
||||
const content = document.getElementById('cpDetailsContent');
|
||||
|
||||
if (!modal || !content) {
|
||||
console.error('Modal or content element not found');
|
||||
showStatus('Error displaying details modal', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<h6 class="mb-3">CP Code: <span class="cp-code-mono badge bg-primary">${data.cp_code}</span></h6>
|
||||
<p class="text-muted">Total Variations: <strong>${data.count}</strong></p>
|
||||
@@ -536,8 +755,18 @@ function viewCpDetails(cpCode) {
|
||||
`;
|
||||
|
||||
content.innerHTML = html;
|
||||
const modalObj = new bootstrap.Modal(modal);
|
||||
modalObj.show();
|
||||
|
||||
// Get or create Bootstrap Modal instance
|
||||
let modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
if (!modalInstance) {
|
||||
modalInstance = new bootstrap.Modal(modal);
|
||||
}
|
||||
|
||||
// Remove aria-hidden before showing
|
||||
modal.removeAttribute('aria-hidden');
|
||||
|
||||
// Show the modal
|
||||
modalInstance.show();
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user