Implement approved/rejected quantity triggers and warehouse inventory

Database Triggers Implementation:
- Added automatic quantity calculation triggers for scanfg_orders
- Added automatic quantity calculation triggers for scan1_orders (T1 phase)
- Triggers calculate based on CP_base_code grouping (8 digits)
- Quality code: 0 = approved, != 0 = rejected
- Quantities set at insertion time (BEFORE INSERT trigger)
- Added create_triggers() function to initialize_db.py

Warehouse Inventory Enhancement:
- Analyzed old app database quantity calculation logic
- Created comprehensive trigger implementation guide
- Added trigger verification and testing procedures
- Documented data migration strategy

Documentation Added:
- APPROVED_REJECTED_QUANTITIES_ANALYSIS.md - Old app logic analysis
- DATABASE_TRIGGERS_IMPLEMENTATION.md - v2 implementation guide
- WAREHOUSE_INVENTORY_IMPLEMENTATION.md - Inventory view feature

Files Modified:
- initialize_db.py: Added create_triggers() function and call in main()
- Documentation: 3 comprehensive guides for database and inventory management

Quality Metrics:
- Triggers maintain legacy compatibility
- Automatic calculation ensures data consistency
- Performance optimized at database level
- Comprehensive testing documented
This commit is contained in:
Quality App Developer
2026-01-30 12:30:56 +02:00
parent b15cc93b9d
commit 07f77603eb
7 changed files with 2246 additions and 42 deletions

View File

@@ -6,7 +6,8 @@ from app.modules.warehouse.warehouse import (
get_all_locations, add_location, update_location, delete_location,
delete_multiple_locations, get_location_by_id,
search_box_by_number, search_location_with_boxes,
assign_box_to_location, move_box_to_new_location
assign_box_to_location, move_box_to_new_location,
get_cp_inventory_list, search_cp_code, search_by_box_number, get_cp_details
)
import logging
@@ -215,3 +216,120 @@ def api_get_locations():
locations = get_all_locations()
return jsonify({'success': True, 'locations': locations}), 200
# ============================================================================
# API Routes for CP Inventory View
# ============================================================================
@warehouse_bp.route('/api/cp-inventory', methods=['GET'], endpoint='api_cp_inventory')
def api_cp_inventory():
"""Get CP inventory list - all CP articles with box and location info"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
# Validate pagination parameters
if limit > 1000:
limit = 1000
if limit < 1:
limit = 50
if offset < 0:
offset = 0
inventory = get_cp_inventory_list(limit=limit, offset=offset)
return jsonify({
'success': True,
'inventory': inventory,
'count': len(inventory),
'limit': limit,
'offset': offset
}), 200
except Exception as e:
logger.error(f"Error getting CP inventory: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/search-cp', methods=['POST'], endpoint='api_search_cp')
def api_search_cp():
"""Search for CP code in warehouse inventory"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
data = request.get_json()
cp_code = data.get('cp_code', '').strip()
if not cp_code or len(cp_code) < 2:
return jsonify({'success': False, 'error': 'CP code must be at least 2 characters'}), 400
results = search_cp_code(cp_code)
return jsonify({
'success': True,
'results': results,
'count': len(results),
'search_term': cp_code
}), 200
except Exception as e:
logger.error(f"Error searching CP code: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/search-cp-box', methods=['POST'], endpoint='api_search_cp_box')
def api_search_cp_box():
"""Search for box number and get all CP codes in it"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
data = request.get_json()
box_number = data.get('box_number', '').strip()
if not box_number or len(box_number) < 1:
return jsonify({'success': False, 'error': 'Box number is required'}), 400
results = search_by_box_number(box_number)
return jsonify({
'success': True,
'results': results,
'count': len(results),
'search_term': box_number
}), 200
except Exception as e:
logger.error(f"Error searching by box number: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/cp-details/<cp_code>', methods=['GET'], endpoint='api_cp_details')
def api_cp_details(cp_code):
"""Get detailed information for a CP code and all its variations"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
cp_code = cp_code.strip().upper()
# Ensure CP code is properly formatted (at least "CP00000001")
if not cp_code.startswith('CP') or len(cp_code) < 10:
return jsonify({'success': False, 'error': 'Invalid CP code format'}), 400
# Extract base CP code (first 10 characters)
cp_base = cp_code[:10]
details = get_cp_details(cp_base)
return jsonify({
'success': True,
'cp_code': cp_base,
'details': details,
'count': len(details)
}), 200
except Exception as e:
logger.error(f"Error getting CP details: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

View File

@@ -3,6 +3,7 @@ Warehouse Module - Helper Functions
Provides functions for warehouse operations
"""
import logging
import pymysql
from app.database import get_db
logger = logging.getLogger(__name__)
@@ -435,3 +436,196 @@ def move_box_to_new_location(box_id, new_location_code):
except Exception as e:
logger.error(f"Error moving box: {e}")
return False, f'Error: {str(e)}', 500
def get_cp_inventory_list(limit=100, offset=0):
"""
Get CP articles from scanfg_orders with box and location info
Groups by CP_full_code (8 digits) to show all entries with that base CP
Returns latest entries first
Returns:
List of CP inventory records with box and location info
"""
try:
conn = get_db()
cursor = conn.cursor(pymysql.cursors.DictCursor)
# Get all CP codes with their box and location info (latest entries first)
cursor.execute("""
SELECT
s.id,
s.CP_full_code,
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
COUNT(*) as total_entries,
s.box_id,
bc.box_number,
wl.location_code,
wl.id as location_id,
MAX(s.date) as latest_date,
MAX(s.time) as latest_time,
SUM(s.approved_quantity) as total_approved,
SUM(s.rejected_quantity) as total_rejected
FROM scanfg_orders s
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
ORDER BY MAX(s.created_at) DESC
LIMIT %s OFFSET %s
""", (limit, offset))
results = cursor.fetchall()
cursor.close()
return results if results else []
except Exception as e:
logger.error(f"Error getting CP inventory: {e}")
return []
def search_cp_code(cp_code_search):
"""
Search for CP codes - can search by full CP code or CP base (8 digits)
Args:
cp_code_search: Search string (can be "CP00000001" or "CP00000001-0001")
Returns:
List of matching CP inventory records
"""
try:
conn = get_db()
cursor = conn.cursor(pymysql.cursors.DictCursor)
# Remove hyphen and get base CP code if full CP provided
search_term = cp_code_search.replace('-', '').strip().upper()
# Search for matching CP codes
cursor.execute("""
SELECT
s.id,
s.CP_full_code,
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
COUNT(*) as total_entries,
s.box_id,
bc.box_number,
wl.location_code,
wl.id as location_id,
MAX(s.date) as latest_date,
MAX(s.time) as latest_time,
SUM(s.approved_quantity) as total_approved,
SUM(s.rejected_quantity) as total_rejected
FROM scanfg_orders s
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
WHERE REPLACE(s.CP_full_code, '-', '') LIKE %s
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
ORDER BY MAX(s.created_at) DESC
""", (f"{search_term}%",))
results = cursor.fetchall()
cursor.close()
return results if results else []
except Exception as e:
logger.error(f"Error searching CP code: {e}")
return []
def search_by_box_number(box_number_search):
"""
Search for box number and get all CP codes in that box
Args:
box_number_search: Box number to search for
Returns:
List of CP entries in the box
"""
try:
conn = get_db()
cursor = conn.cursor(pymysql.cursors.DictCursor)
box_search = box_number_search.strip().upper()
cursor.execute("""
SELECT
s.id,
s.CP_full_code,
s.operator_code,
s.quality_code,
s.date,
s.time,
s.approved_quantity,
s.rejected_quantity,
bc.box_number,
bc.id as box_id,
wl.location_code,
wl.id as location_id,
s.created_at
FROM scanfg_orders s
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
WHERE bc.box_number LIKE %s
ORDER BY s.created_at DESC
LIMIT 500
""", (f"%{box_search}%",))
results = cursor.fetchall()
cursor.close()
return results if results else []
except Exception as e:
logger.error(f"Error searching by box number: {e}")
return []
def get_cp_details(cp_code):
"""
Get detailed information for a specific CP code (8 digits)
Shows all variations (with different 4-digit suffixes) and their locations
Args:
cp_code: CP base code (8 digits, e.g., "CP00000001")
Returns:
List of all CP variations with their box and location info
"""
try:
conn = get_db()
cursor = conn.cursor(pymysql.cursors.DictCursor)
# Search for all entries with this CP base
cursor.execute("""
SELECT
s.id,
s.CP_full_code,
s.operator_code,
s.OC1_code,
s.OC2_code,
s.quality_code,
s.date,
s.time,
s.approved_quantity,
s.rejected_quantity,
bc.box_number,
bc.id as box_id,
wl.location_code,
wl.id as location_id,
wl.description,
s.created_at
FROM scanfg_orders s
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
WHERE SUBSTRING(s.CP_full_code, 1, 10) = %s
ORDER BY s.created_at DESC
LIMIT 1000
""", (cp_code.upper(),))
results = cursor.fetchall()
cursor.close()
return results if results else []
except Exception as e:
logger.error(f"Error getting CP details: {e}")
return []

View File

@@ -1,67 +1,552 @@
{% extends "base.html" %}
{% block title %}Warehouse Inventory - Quality App v2{% endblock %}
{% block title %}Warehouse Inventory - CP Articles{% endblock %}
{% block content %}
<div class="container-fluid py-5">
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-2">
<i class="fas fa-list"></i> Warehouse Inventory
</h1>
<p class="text-muted">Search and view products, boxes, and their warehouse locations</p>
</div>
<a href="{{ url_for('warehouse.warehouse_index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Warehouse
</a>
</div>
<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-12">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-search"></i> Search Inventory</h5>
<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="row">
<div class="col-md-6">
<div class="form-group">
<label for="searchProduct">Search by Product Code:</label>
<input type="text" id="searchProduct" class="form-control" placeholder="Enter product code...">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="searchLocation">Search by Location:</label>
<input type="text" id="searchLocation" class="form-control" placeholder="Enter location code...">
</div>
</div>
<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>
<button class="btn btn-primary">
<i class="fas fa-search"></i> Search
</button>
<small class="text-muted d-block mt-2">
Searches for any CP code starting with the entered text. Shows all related entries.
</small>
</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>
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-box"></i> Inventory Results</h5>
<!-- 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">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-details"></i> CP Code Details
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="card-body">
<p class="text-muted">
<i class="fas fa-info-circle"></i> Inventory search feature coming soon...
</p>
<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;
}
</style>
<script>
let currentSearchType = 'all';
let inventoryData = [];
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
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');
// Auto-hide after 5 seconds
setTimeout(() => {
alert.classList.add('d-none');
}, 5000);
}
function showLoading() {
document.getElementById('loadingSpinner').classList.remove('d-none');
}
function hideLoading() {
document.getElementById('loadingSpinner').classList.add('d-none');
}
function loadInventory() {
showLoading();
currentSearchType = 'all';
document.getElementById('cpCodeSearch').value = '';
document.getElementById('boxNumberSearch').value = '';
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);
document.getElementById('totalRecords').textContent = data.count;
document.getElementById('showingRecords').textContent = data.count;
document.getElementById('lastUpdated').textContent = 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 = document.getElementById('cpCodeSearch').value.trim();
if (!cpCode) {
showStatus('Please enter a CP code', 'warning');
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);
document.getElementById('totalRecords').textContent = data.count;
document.getElementById('showingRecords').textContent = 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() {
document.getElementById('cpCodeSearch').value = '';
loadInventory();
}
function searchByBoxNumber() {
const boxNumber = document.getElementById('boxNumberSearch').value.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);
document.getElementById('totalRecords').textContent = data.count;
document.getElementById('showingRecords').textContent = 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() {
document.getElementById('boxNumberSearch').value = '';
loadInventory();
}
function renderTable(data) {
const tbody = document.getElementById('tableBody');
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 (!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');
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;
const modalObj = new bootstrap.Modal(modal);
modalObj.show();
} else {
showStatus(`Error: ${data.error}`, 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showStatus(`Error loading CP details: ${error.message}`, 'danger');
});
}
</script>
{% endblock %}