✅ Admin: Process creation, field configuration, template upload ✅ Staff: Session list, new session (header form), scanning interface ✅ Duplicate detection (same session = blue, other session = orange) ✅ Weight entry popup, edit/delete scans
536 lines
20 KiB
HTML
536 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Scanning - {{ session.process_name }} - ScanLook{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="count-location-container">
|
|
<div class="location-header">
|
|
<div class="location-info">
|
|
<a href="{{ url_for('cons_sheets.index') }}" class="breadcrumb">← Back to Sessions</a>
|
|
<div class="location-label">{{ session.process_name }}</div>
|
|
<div class="header-values">
|
|
{% for hv in header_values %}
|
|
<span class="header-pill"><strong>{{ hv.field_label }}:</strong> {{ hv.field_value }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="location-stats">
|
|
<span class="stat-pill">Scanned: <span id="scanCount">{{ scans|length }}</span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scan Input Card -->
|
|
<div class="scan-card scan-card-active">
|
|
<div class="scan-header">
|
|
<h2 class="scan-title">Scan Lot Number</h2>
|
|
</div>
|
|
|
|
<form id="lotScanForm" class="scan-form">
|
|
<div class="scan-input-group">
|
|
<input type="text"
|
|
name="lot_number"
|
|
id="lotInput"
|
|
inputmode="none"
|
|
class="scan-input"
|
|
placeholder="Scan Lot Number"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
autofocus>
|
|
<button type="submit" style="display: none;"></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Duplicate Confirmation Modal (Same Session - Blue) -->
|
|
<div id="duplicateSameModal" class="modal">
|
|
<div class="modal-content modal-duplicate">
|
|
<div class="duplicate-lot-number" id="dupSameLotNumber"></div>
|
|
<h3 class="duplicate-title" style="color: var(--color-duplicate);">Already Scanned</h3>
|
|
<p class="duplicate-message">This lot was already scanned in this session.<br>Add it again?</p>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-secondary btn-lg" onclick="cancelDuplicate()">No</button>
|
|
<button type="button" class="btn btn-lg" style="background: var(--color-duplicate); color: white;" onclick="confirmDuplicate()">Yes, Add</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Duplicate Confirmation Modal (Other Session - Orange) -->
|
|
<div id="duplicateOtherModal" class="modal">
|
|
<div class="modal-content modal-duplicate">
|
|
<div class="duplicate-lot-number" id="dupOtherLotNumber"></div>
|
|
<h3 class="duplicate-title" style="color: var(--color-warning);">⚠️ Previously Consumed</h3>
|
|
<p class="duplicate-message" id="dupOtherInfo"></p>
|
|
<p class="duplicate-message">Are you sure you want to add it?</p>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-secondary btn-lg" onclick="cancelDuplicate()">No</button>
|
|
<button type="button" class="btn btn-lg" style="background: var(--color-warning); color: var(--color-bg);" onclick="confirmDuplicate()">Yes, Add</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Weight Entry Modal -->
|
|
<div id="weightModal" class="modal">
|
|
<div class="modal-content">
|
|
<h3 class="modal-title">Enter Weight</h3>
|
|
<div class="modal-lot-info">
|
|
<div id="modalLotNumber" class="modal-lot-number"></div>
|
|
</div>
|
|
<form id="weightForm" class="weight-form">
|
|
<div class="form-group">
|
|
<label class="form-label">Item Number (optional)</label>
|
|
<input type="text" id="itemInput" class="form-input" placeholder="Item/SKU">
|
|
</div>
|
|
<input
|
|
type="number"
|
|
id="weightInput"
|
|
class="weight-input"
|
|
placeholder="Weight (lbs)"
|
|
step="0.01"
|
|
min="0"
|
|
inputmode="decimal"
|
|
autocomplete="off"
|
|
required
|
|
>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="cancelWeight()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scanned Lots Grid -->
|
|
<div class="scans-section">
|
|
<div class="scans-header">
|
|
<h3 class="scans-title">Scanned Lots (<span id="scanListCount">{{ scans|length }}</span>)</h3>
|
|
</div>
|
|
|
|
<div id="scansList" class="scans-grid">
|
|
{% for scan in scans %}
|
|
<div class="scan-row scan-row-{{ scan.duplicate_status }}"
|
|
data-detail-id="{{ scan.id }}"
|
|
onclick="openScanDetail({{ scan.id }})">
|
|
<div class="scan-row-lot">{{ scan.lot_number }}</div>
|
|
<div class="scan-row-item">{{ scan.item_number or 'N/A' }}</div>
|
|
<div class="scan-row-weight">{{ '%.1f'|format(scan.weight) if scan.weight else '-' }} lbs</div>
|
|
<div class="scan-row-status">
|
|
{% if scan.duplicate_status == 'dup_same_session' %}
|
|
<span class="status-dot status-dot-blue"></span> Duplicate
|
|
{% elif scan.duplicate_status == 'dup_other_session' %}
|
|
<span class="status-dot status-dot-orange"></span> Warning
|
|
{% else %}
|
|
<span class="status-dot status-dot-green"></span> OK
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scan Detail/Edit Modal -->
|
|
<div id="scanDetailModal" class="modal">
|
|
<div class="modal-content modal-large">
|
|
<div class="modal-header-bar">
|
|
<h3 class="modal-title">Scan Details</h3>
|
|
<button type="button" class="btn-close-modal" onclick="closeScanDetail()">✕</button>
|
|
</div>
|
|
<div id="scanDetailContent" class="scan-detail-content"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="finish-section">
|
|
<div class="action-buttons-row">
|
|
<a href="{{ url_for('cons_sheets.index') }}" class="btn btn-secondary btn-block btn-lg">
|
|
← Back to Sessions
|
|
</a>
|
|
<button class="btn btn-success btn-block btn-lg" onclick="exportToExcel()">
|
|
📊 Export to Excel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.header-values {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-sm);
|
|
margin: var(--space-sm) 0;
|
|
}
|
|
|
|
.header-pill {
|
|
background: var(--color-surface-elevated);
|
|
padding: var(--space-xs) var(--space-sm);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.8rem;
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.header-pill strong {
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.scan-row-dup_same_session {
|
|
border-left: 4px solid var(--color-duplicate) !important;
|
|
background: rgba(0, 163, 255, 0.1) !important;
|
|
}
|
|
|
|
.scan-row-dup_other_session {
|
|
border-left: 4px solid var(--color-warning) !important;
|
|
background: rgba(255, 170, 0, 0.1) !important;
|
|
}
|
|
|
|
.scan-row-normal {
|
|
border-left: 4px solid var(--color-success);
|
|
}
|
|
|
|
.modal-duplicate {
|
|
text-align: center;
|
|
padding: var(--space-xl);
|
|
}
|
|
|
|
.duplicate-lot-number {
|
|
font-family: var(--font-mono);
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--color-primary);
|
|
margin-bottom: var(--space-md);
|
|
}
|
|
|
|
.duplicate-title {
|
|
font-size: 1.25rem;
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
|
|
.duplicate-message {
|
|
color: var(--color-text-muted);
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let currentLotNumber = '';
|
|
let currentDuplicateStatus = '';
|
|
let currentDuplicateInfo = '';
|
|
let isDuplicateConfirmed = false;
|
|
let isProcessing = false;
|
|
|
|
// Lot scan handler
|
|
document.getElementById('lotScanForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
if (isProcessing) return;
|
|
|
|
const scannedValue = document.getElementById('lotInput').value.trim();
|
|
if (!scannedValue) return;
|
|
|
|
isProcessing = true;
|
|
currentLotNumber = scannedValue;
|
|
document.getElementById('lotInput').value = '';
|
|
|
|
checkDuplicate();
|
|
});
|
|
|
|
function checkDuplicate() {
|
|
fetch('{{ url_for("cons_sheets.scan_lot", session_id=session.id) }}', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
lot_number: currentLotNumber,
|
|
check_only: true
|
|
})
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.needs_confirmation) {
|
|
currentDuplicateStatus = data.duplicate_status;
|
|
currentDuplicateInfo = data.duplicate_info;
|
|
|
|
if (data.duplicate_status === 'dup_same_session') {
|
|
document.getElementById('dupSameLotNumber').textContent = currentLotNumber;
|
|
document.getElementById('duplicateSameModal').style.display = 'flex';
|
|
} else if (data.duplicate_status === 'dup_other_session') {
|
|
document.getElementById('dupOtherLotNumber').textContent = currentLotNumber;
|
|
document.getElementById('dupOtherInfo').textContent = data.duplicate_info;
|
|
document.getElementById('duplicateOtherModal').style.display = 'flex';
|
|
}
|
|
} else {
|
|
showWeightModal();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showWeightModal();
|
|
});
|
|
}
|
|
|
|
function confirmDuplicate() {
|
|
isDuplicateConfirmed = true;
|
|
document.getElementById('duplicateSameModal').style.display = 'none';
|
|
document.getElementById('duplicateOtherModal').style.display = 'none';
|
|
showWeightModal();
|
|
}
|
|
|
|
function cancelDuplicate() {
|
|
document.getElementById('duplicateSameModal').style.display = 'none';
|
|
document.getElementById('duplicateOtherModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
isDuplicateConfirmed = false;
|
|
isProcessing = false;
|
|
currentDuplicateStatus = '';
|
|
}
|
|
|
|
function showWeightModal() {
|
|
document.getElementById('modalLotNumber').textContent = currentLotNumber;
|
|
document.getElementById('weightModal').style.display = 'flex';
|
|
document.getElementById('itemInput').value = '';
|
|
document.getElementById('weightInput').value = '';
|
|
document.getElementById('weightInput').focus();
|
|
}
|
|
|
|
function cancelWeight() {
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
isDuplicateConfirmed = false;
|
|
isProcessing = false;
|
|
currentDuplicateStatus = '';
|
|
}
|
|
|
|
// Weight form handler
|
|
document.getElementById('weightForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const weight = document.getElementById('weightInput').value;
|
|
const itemNumber = document.getElementById('itemInput').value.trim();
|
|
|
|
if (!weight || weight <= 0) {
|
|
alert('Please enter a valid weight');
|
|
return;
|
|
}
|
|
|
|
submitScan(itemNumber, weight);
|
|
});
|
|
|
|
function submitScan(itemNumber, weight) {
|
|
fetch('{{ url_for("cons_sheets.scan_lot", session_id=session.id) }}', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
lot_number: currentLotNumber,
|
|
item_number: itemNumber,
|
|
weight: parseFloat(weight),
|
|
confirm_duplicate: isDuplicateConfirmed
|
|
})
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
|
|
// Update existing rows if needed
|
|
if (data.updated_entry_ids && data.updated_entry_ids.length > 0) {
|
|
data.updated_entry_ids.forEach(id => {
|
|
const row = document.querySelector(`[data-detail-id="${id}"]`);
|
|
if (row) {
|
|
row.className = 'scan-row scan-row-dup_same_session';
|
|
row.querySelector('.scan-row-status').innerHTML = '<span class="status-dot status-dot-blue"></span> Duplicate';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add new row
|
|
addScanToList(data.detail_id, currentLotNumber, itemNumber, weight, data.duplicate_status);
|
|
|
|
// Reset state
|
|
currentLotNumber = '';
|
|
isDuplicateConfirmed = false;
|
|
isProcessing = false;
|
|
currentDuplicateStatus = '';
|
|
document.getElementById('lotInput').focus();
|
|
} else {
|
|
alert(data.message || 'Error saving scan');
|
|
isProcessing = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error saving scan');
|
|
isProcessing = false;
|
|
});
|
|
}
|
|
|
|
function addScanToList(detailId, lotNumber, itemNumber, weight, duplicateStatus) {
|
|
const scansList = document.getElementById('scansList');
|
|
|
|
let statusClass = duplicateStatus || 'normal';
|
|
let statusDot = 'green';
|
|
let statusText = 'OK';
|
|
|
|
if (duplicateStatus === 'dup_same_session') {
|
|
statusDot = 'blue';
|
|
statusText = 'Duplicate';
|
|
} else if (duplicateStatus === 'dup_other_session') {
|
|
statusDot = 'orange';
|
|
statusText = 'Warning';
|
|
}
|
|
|
|
const scanRow = document.createElement('div');
|
|
scanRow.className = 'scan-row scan-row-' + statusClass;
|
|
scanRow.setAttribute('data-detail-id', detailId);
|
|
scanRow.onclick = function() { openScanDetail(detailId); };
|
|
scanRow.innerHTML = `
|
|
<div class="scan-row-lot">${lotNumber}</div>
|
|
<div class="scan-row-item">${itemNumber || 'N/A'}</div>
|
|
<div class="scan-row-weight">${parseFloat(weight).toFixed(1)} lbs</div>
|
|
<div class="scan-row-status">
|
|
<span class="status-dot status-dot-${statusDot}"></span> ${statusText}
|
|
</div>
|
|
`;
|
|
|
|
scansList.insertBefore(scanRow, scansList.firstChild);
|
|
|
|
// Update count
|
|
const countSpan = document.getElementById('scanListCount');
|
|
const scanCountSpan = document.getElementById('scanCount');
|
|
const newCount = scansList.children.length;
|
|
if (countSpan) countSpan.textContent = newCount;
|
|
if (scanCountSpan) scanCountSpan.textContent = newCount;
|
|
}
|
|
|
|
function openScanDetail(detailId) {
|
|
fetch('/cons-sheets/detail/' + detailId)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayScanDetail(data.detail);
|
|
} else {
|
|
alert('Error loading details');
|
|
}
|
|
});
|
|
}
|
|
|
|
function displayScanDetail(detail) {
|
|
const content = document.getElementById('scanDetailContent');
|
|
|
|
let statusBadge = '<span class="badge badge-success">✓ OK</span>';
|
|
if (detail.duplicate_status === 'dup_same_session') {
|
|
statusBadge = '<span class="badge badge-duplicate">🔵 Duplicate (Same Session)</span>';
|
|
} else if (detail.duplicate_status === 'dup_other_session') {
|
|
statusBadge = '<span class="badge badge-warning">🟠 Previously Consumed</span>';
|
|
}
|
|
|
|
content.innerHTML = `
|
|
<div class="detail-section">
|
|
<div class="detail-row">
|
|
<span class="detail-label">Lot Number:</span>
|
|
<span class="detail-value detail-lot">${detail.lot_number}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Status:</span>
|
|
<span class="detail-value">${statusBadge}</span>
|
|
</div>
|
|
${detail.duplicate_info ? `<div class="detail-row"><span class="detail-label">Info:</span><span class="detail-value">${detail.duplicate_info}</span></div>` : ''}
|
|
<div class="detail-row">
|
|
<span class="detail-label">Scanned:</span>
|
|
<span class="detail-value">${detail.scanned_at} by ${detail.scanned_by_name}</span>
|
|
</div>
|
|
</div>
|
|
<div class="detail-section">
|
|
<h4 class="detail-section-title">Edit Scan</h4>
|
|
<div class="detail-form">
|
|
<div class="form-group">
|
|
<label class="form-label">Item Number</label>
|
|
<input type="text" id="editItem" class="form-input" value="${detail.item_number || ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Lot Number</label>
|
|
<input type="text" id="editLot" class="form-input" value="${detail.lot_number || ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Weight (lbs)</label>
|
|
<input type="number" id="editWeight" class="form-input" value="${detail.weight || ''}" step="0.01" min="0">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Comment</label>
|
|
<textarea id="editComment" class="form-textarea" rows="3">${detail.comment || ''}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="detail-actions">
|
|
<button class="btn btn-secondary" onclick="closeScanDetail()">Cancel</button>
|
|
<button class="btn btn-danger" onclick="deleteDetail(${detail.id})">Delete</button>
|
|
<button class="btn btn-primary" onclick="saveDetail(${detail.id})">Save Changes</button>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('scanDetailModal').style.display = 'flex';
|
|
}
|
|
|
|
function closeScanDetail() {
|
|
document.getElementById('scanDetailModal').style.display = 'none';
|
|
}
|
|
|
|
function saveDetail(detailId) {
|
|
const item = document.getElementById('editItem').value.trim();
|
|
const lot = document.getElementById('editLot').value.trim();
|
|
const weight = document.getElementById('editWeight').value;
|
|
const comment = document.getElementById('editComment').value;
|
|
|
|
if (!lot) {
|
|
alert('Lot number is required');
|
|
return;
|
|
}
|
|
|
|
fetch('/cons-sheets/detail/' + detailId + '/update', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ item_number: item, lot_number: lot, weight: parseFloat(weight), comment: comment })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
closeScanDetail();
|
|
location.reload();
|
|
} else {
|
|
alert(data.message || 'Error updating');
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteDetail(detailId) {
|
|
if (!confirm('Delete this scan?')) return;
|
|
|
|
fetch('/cons-sheets/detail/' + detailId + '/delete', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'}
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
closeScanDetail();
|
|
location.reload();
|
|
} else {
|
|
alert(data.message || 'Error deleting');
|
|
}
|
|
});
|
|
}
|
|
|
|
function exportToExcel() {
|
|
alert('Excel export coming soon! For now, you can view and print the data from the admin panel.');
|
|
// TODO: Implement Excel export
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
document.getElementById('duplicateSameModal').style.display = 'none';
|
|
document.getElementById('duplicateOtherModal').style.display = 'none';
|
|
document.getElementById('scanDetailModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|