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:
Quality App Developer
2026-02-02 01:06:03 +02:00
parent 39a3a0084c
commit f54e1bebc3
7 changed files with 1986 additions and 52 deletions

View File

@@ -30,6 +30,22 @@
</div>
</div>
<!-- Set Orders on Boxes Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">
<div class="card-body text-center">
<div class="launcher-icon mb-3">
<i class="fas fa-archive text-secondary"></i>
</div>
<h5 class="card-title">Set Orders on Boxes</h5>
<p class="card-text text-muted">Assign, move, or view orders on boxes and manage order-to-box relationships.</p>
<a href="{{ url_for('warehouse.set_orders_on_boxes') }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-right"></i> Manage Orders
</a>
</div>
</div>
</div>
<!-- Create Warehouse Locations Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">

View File

@@ -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');
}

View File

@@ -722,6 +722,7 @@
async function assignBoxToLocationTab0() {
const locationCode = document.getElementById('location-code-input-tab0').value.trim();
const boxNumber = document.getElementById('box-number-input-tab0').value.trim();
if (!currentBoxId_Tab0) {
showAlert('alert-tab0', 'Please search for a box first', 'error');
@@ -751,8 +752,36 @@
const data = await response.json();
if (data.success) {
showAlert('alert-tab0', data.message, 'success');
setTimeout(() => clearTab0(), 1500);
showAlert('alert-tab0', data.message + ' - Refreshing box info...', 'success');
// Clear location input but keep box number to refresh details
document.getElementById('location-code-input-tab0').value = '';
document.getElementById('assign-section-tab0').style.display = 'none';
// Automatically refresh box details after a brief delay
setTimeout(async () => {
try {
const searchResponse = await fetch('{{ url_for("warehouse.api_search_box") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ box_number: boxNumber })
});
const searchData = await searchResponse.json();
if (searchData.success) {
document.getElementById('display-box-number-tab0').textContent = searchData.box.box_number;
document.getElementById('display-box-status-tab0').textContent = searchData.box.status;
document.getElementById('display-box-location-tab0').textContent = searchData.box.location_code || 'Unassigned';
document.getElementById('box-info-tab0').classList.add('show');
showAlert('alert-tab0', 'Box updated successfully', 'success');
}
} catch (error) {
console.error('Error refreshing box:', error);
}
}, 800);
} else {
showAlert('alert-tab0', data.message || 'Error assigning box', 'error');
}
@@ -932,6 +961,7 @@
}
async function moveBoxTab2() {
const locationCode = document.getElementById('location-code-input-tab2').value.trim();
const newLocationCode = document.getElementById('new-location-input-tab2').value.trim();
if (!currentBoxId_Tab2) {
@@ -962,8 +992,45 @@
const data = await response.json();
if (data.success) {
showAlert('alert-tab2', data.message, 'success');
setTimeout(() => clearTab2(), 1500);
showAlert('alert-tab2', data.message + ' - Refreshing location...', 'success');
// Clear the selection but keep the location code to show updated list
document.getElementById('new-location-input-tab2').value = '';
document.getElementById('move-section-tab2').style.display = 'none';
document.getElementById('selected-box-display-tab2').textContent = '-';
currentBoxId_Tab2 = null;
// Automatically refresh and show the updated location list after a brief delay
setTimeout(async () => {
try {
const searchResponse = await fetch('{{ url_for("warehouse.api_search_location") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ location_code: locationCode })
});
const searchData = await searchResponse.json();
if (searchData.success) {
document.getElementById('display-location-code-tab2').textContent = searchData.location.location_code;
document.getElementById('display-boxes-count-tab2').textContent = searchData.boxes.length;
document.getElementById('location-info-tab2').classList.add('show');
if (searchData.boxes.length > 0) {
displayBoxesTab2(searchData.boxes);
document.getElementById('boxes-list-tab2').classList.add('show');
showAlert('alert-tab2', `Location updated - ${searchData.boxes.length} box(es) found`, 'success');
} else {
document.getElementById('boxes-list-tab2').classList.remove('show');
showAlert('alert-tab2', `Location "${locationCode}" is now empty`, 'warning');
}
}
} catch (error) {
console.error('Error refreshing location:', error);
}
}, 800);
} else {
showAlert('alert-tab2', data.message || 'Error moving box', 'error');
}

File diff suppressed because it is too large Load Diff