- 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
782 lines
26 KiB
HTML
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 %}
|