Files
quality_app-v2/app/templates/modules/warehouse/inventory.html
Quality App Developer f54e1bebc3 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
2026-02-02 01:06:03 +02:00

782 lines
26 KiB
HTML

{% extends "base.html" %}
{% block title %}Warehouse Inventory - CP Articles{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-12">
<h1 class="h3 mb-3">
<i class="fas fa-box"></i> Warehouse Inventory
</h1>
<p class="text-muted">View CP articles in warehouse with box numbers and locations. Latest entries displayed first.</p>
</div>
</div>
<!-- Search Section -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-search"></i> Search by CP Code</h5>
</div>
<div class="card-body">
<div class="input-group">
<input type="text"
id="cpCodeSearch"
class="form-control"
placeholder="Enter CP code (e.g., CP00000001 or CP00000001-0001)"
autocomplete="off">
<button class="btn btn-primary"
type="button"
id="searchCpBtn"
onclick="searchByCpCode()">
<i class="fas fa-search"></i> Search CP
</button>
<button class="btn btn-secondary"
type="button"
onclick="clearCpSearch()">
<i class="fas fa-times"></i> Clear
</button>
</div>
<small class="text-muted d-block mt-2">
<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>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-boxes"></i> Search by Box Number</h5>
</div>
<div class="card-body">
<div class="input-group">
<input type="text"
id="boxNumberSearch"
class="form-control"
placeholder="Enter box number (e.g., BOX001)"
autocomplete="off">
<button class="btn btn-info"
type="button"
id="searchBoxBtn"
onclick="searchByBoxNumber()">
<i class="fas fa-search"></i> Search Box
</button>
<button class="btn btn-secondary"
type="button"
onclick="clearBoxSearch()">
<i class="fas fa-times"></i> Clear
</button>
</div>
<small class="text-muted d-block mt-2">
Find all CP codes in a specific box with location and operator info.
</small>
</div>
</div>
</div>
</div>
<!-- Status Messages -->
<div id="statusAlert" class="alert alert-info d-none" role="alert">
<i class="fas fa-info-circle"></i> <span id="statusMessage"></span>
</div>
<!-- Loading Indicator -->
<div id="loadingSpinner" class="spinner-border d-none" role="status" style="display: none;">
<span class="sr-only">Loading...</span>
</div>
<!-- Results Table -->
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-table"></i> CP Inventory</h5>
<button class="btn btn-sm btn-light" onclick="reloadInventory()">
<i class="fas fa-sync"></i> Reload
</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="inventoryTable">
<thead class="bg-light sticky-top">
<tr>
<th class="bg-primary text-white">CP Code (Base)</th>
<th class="bg-primary text-white">CP Full Code</th>
<th class="bg-success text-white">Box Number</th>
<th class="bg-info text-white">Location</th>
<th class="bg-warning text-dark">Total Entries</th>
<th class="bg-secondary text-white">Approved Qty</th>
<th class="bg-secondary text-white">Rejected Qty</th>
<th class="bg-secondary text-white">Latest Date</th>
<th class="bg-secondary text-white">Latest Time</th>
<th class="bg-dark text-white">Actions</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td colspan="10" class="text-center text-muted py-5">
<i class="fas fa-spinner fa-spin"></i> Loading inventory data...
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer text-muted">
<small>
Total Records: <strong id="totalRecords">0</strong> |
Showing: <strong id="showingRecords">0</strong> |
Last Updated: <strong id="lastUpdated">-</strong>
</small>
</div>
</div>
<!-- CP Details Modal -->
<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" 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" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="cpDetailsContent">
<i class="fas fa-spinner fa-spin"></i> Loading details...
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.sticky-top {
top: 0;
z-index: 10;
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,0.05);
}
.badge {
font-size: 0.85rem;
padding: 0.4rem 0.6rem;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.search-box {
border-radius: 0.25rem;
}
.cp-code-mono {
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>
let currentSearchType = 'all';
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();
});
// Validate CP Code input - must start with "CP"
function validateCpCodeInput(value) {
const validationDiv = document.getElementById('cpCodeValidation');
const validationText = document.getElementById('cpCodeValidationText');
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() {
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() {
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';
// 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 => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
})
.then(data => {
if (data.success) {
inventoryData = data.inventory;
renderTable(data.inventory);
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');
}
})
.catch(error => {
console.error('Error:', error);
showStatus(`Error loading inventory: ${error.message}`, 'danger');
})
.finally(() => hideLoading());
}
function reloadInventory() {
loadInventory();
}
function searchByCpCode() {
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';
fetch('/warehouse/api/search-cp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ cp_code: cpCode })
})
.then(response => response.json())
.then(data => {
if (data.success) {
inventoryData = data.results;
renderTable(data.results);
safeSetElementText('totalRecords', data.count);
safeSetElementText('showingRecords', data.count);
showStatus(`Found ${data.count} entries for CP code: ${cpCode}`, 'success');
} else {
showStatus(`Error: ${data.error}`, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus(`Error searching CP code: ${error.message}`, 'danger');
})
.finally(() => hideLoading());
}
function clearCpSearch() {
safeSetElementValue('cpCodeSearch', '');
loadInventory();
}
function searchByBoxNumber() {
const boxNumber = safeGetElementValue('boxNumberSearch').trim();
if (!boxNumber) {
showStatus('Please enter a box number', 'warning');
return;
}
showLoading();
currentSearchType = 'box';
fetch('/warehouse/api/search-cp-box', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ box_number: boxNumber })
})
.then(response => response.json())
.then(data => {
if (data.success) {
inventoryData = data.results;
renderBoxSearchTable(data.results);
safeSetElementText('totalRecords', data.count);
safeSetElementText('showingRecords', data.count);
showStatus(`Found ${data.count} CP entries in box: ${boxNumber}`, 'success');
} else {
showStatus(`Error: ${data.error}`, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus(`Error searching by box number: ${error.message}`, 'danger');
})
.finally(() => hideLoading());
}
function clearBoxSearch() {
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;
}
tbody.innerHTML = data.map(item => `
<tr>
<td>
<span class="cp-code-mono badge bg-primary">
${item.cp_base || 'N/A'}
</span>
</td>
<td>
<span class="cp-code-mono">
${item.CP_full_code || 'N/A'}
</span>
</td>
<td>
<span class="badge bg-success">
${item.box_number ? `BOX ${item.box_number}` : 'No Box'}
</span>
</td>
<td>
<span class="badge bg-info">
${item.location_code || 'No Location'}
</span>
</td>
<td>
<span class="badge bg-warning text-dark">
${item.total_entries || 0}
</span>
</td>
<td>
<span class="badge bg-success">
${item.total_approved || 0}
</span>
</td>
<td>
<span class="badge bg-danger">
${item.total_rejected || 0}
</span>
</td>
<td>
<small>${formatDate(item.latest_date)}</small>
</td>
<td>
<small>${item.latest_time || '-'}</small>
</td>
<td>
<button class="btn btn-sm btn-outline-primary"
onclick="viewCpDetails('${item.cp_base || item.CP_full_code}')"
title="View CP details">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`).join('');
}
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;
}
tbody.innerHTML = data.map(item => `
<tr>
<td>
<span class="cp-code-mono badge bg-primary">
${item.CP_full_code ? item.CP_full_code.substring(0, 10) : 'N/A'}
</span>
</td>
<td>
<span class="cp-code-mono">
${item.CP_full_code || 'N/A'}
</span>
</td>
<td>
<span class="badge bg-success">
${item.box_number ? `BOX ${item.box_number}` : 'No Box'}
</span>
</td>
<td>
<span class="badge bg-info">
${item.location_code || 'No Location'}
</span>
</td>
<td>
<span class="badge bg-warning text-dark">
1
</span>
</td>
<td>
<span class="badge bg-success">
${item.approved_quantity || 0}
</span>
</td>
<td>
<span class="badge bg-danger">
${item.rejected_quantity || 0}
</span>
</td>
<td>
<small>${formatDate(item.date)}</small>
</td>
<td>
<small>${item.time || '-'}</small>
</td>
<td>
<button class="btn btn-sm btn-outline-primary"
onclick="viewCpDetails('${item.CP_full_code ? item.CP_full_code.substring(0, 10) : ''}')"
title="View CP details">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`).join('');
}
function formatDate(dateStr) {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
} catch {
return dateStr;
}
}
function viewCpDetails(cpCode) {
const cleanCpCode = cpCode.replace('-', '').substring(0, 10);
fetch(`/warehouse/api/cp-details/${cleanCpCode}`)
.then(response => response.json())
.then(data => {
if (data.success) {
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>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>CP Full Code</th>
<th>Operator</th>
<th>Quality</th>
<th>Box</th>
<th>Location</th>
<th>Date</th>
<th>Time</th>
</tr>
</thead>
<tbody>
`;
data.details.forEach(item => {
html += `
<tr>
<td><span class="cp-code-mono">${item.CP_full_code}</span></td>
<td>${item.operator_code || '-'}</td>
<td>
<span class="badge ${item.quality_code === '1' ? 'bg-success' : 'bg-danger'}">
${item.quality_code === '1' ? 'Approved' : 'Rejected'}
</span>
</td>
<td>${item.box_number || 'No Box'}</td>
<td>${item.location_code || 'No Location'}</td>
<td><small>${formatDate(item.date)}</small></td>
<td><small>${item.time || '-'}</small></td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
content.innerHTML = html;
// 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');
}
})
.catch(error => {
console.error('Error:', error);
showStatus(`Error loading CP details: ${error.message}`, 'danger');
});
}
</script>
{% endblock %}