582 lines
22 KiB
HTML
582 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Counting {{ location.location_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="count-location-container">
|
|
<div class="location-header">
|
|
<div class="location-info">
|
|
<div class="location-label">Counting Location</div>
|
|
<h1 class="location-name">{{ location.location_name }}</h1>
|
|
<div class="location-stats">
|
|
<span class="stat-pill">Expected: {{ location.expected_lots_master }}</span>
|
|
<span class="stat-pill">Found: <span id="foundCount">{{ location.lots_found }}</span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="scan-card scan-card-active">
|
|
<div class="scan-header">
|
|
<h2 class="scan-title">Scan Lot Barcode</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;" aria-hidden="true"></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="duplicateModal" class="modal">
|
|
<div class="modal-content modal-duplicate">
|
|
<div class="duplicate-lot-number" id="duplicateLotNumber"></div>
|
|
<h3 class="duplicate-title">Already Scanned</h3>
|
|
<p class="duplicate-message">Do you wish to Resubmit?</p>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-secondary btn-lg" onclick="cancelDuplicate()">No</button>
|
|
<button type="button" class="btn btn-primary btn-lg" onclick="confirmDuplicate()">Yes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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 id="modalItemDesc" class="modal-item-desc"></div>
|
|
</div>
|
|
<form id="weightForm" class="weight-form">
|
|
<input
|
|
type="number"
|
|
id="weightInput"
|
|
class="weight-input"
|
|
placeholder="0.0"
|
|
step="0.01"
|
|
min="0"
|
|
inputmode="decimal"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
spellcheck="false"
|
|
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>
|
|
|
|
<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 %}
|
|
|
|
{# LOGIC FIX: Determine CSS class based on weight difference #}
|
|
{% set row_class = scan.master_status %}
|
|
|
|
{% if scan.duplicate_status and scan.duplicate_status != '00' %}
|
|
{% set row_class = 'duplicate-' + scan.duplicate_status %}
|
|
{% elif scan.master_status == 'match' and scan.master_expected_weight and (scan.actual_weight - scan.master_expected_weight)|abs >= 0.01 %}
|
|
{# If it is a match but weight is off, force class to weight_discrepancy #}
|
|
{% set row_class = 'weight_discrepancy' %}
|
|
{% endif %}
|
|
|
|
<div class="scan-row scan-row-{{ row_class }}"
|
|
data-entry-id="{{ scan.entry_id }}"
|
|
onclick="openScanDetail('{{ scan.entry_id }}')">
|
|
<div class="scan-row-lot">{{ scan.lot_number }}</div>
|
|
<div class="scan-row-item">{{ scan.item or 'N/A' }}</div>
|
|
<div class="scan-row-weight">{{ scan.actual_weight }} lbs</div>
|
|
<div class="scan-row-status">
|
|
{% if scan.duplicate_status == '01' or scan.duplicate_status == '04' %}
|
|
<span class="status-dot status-dot-blue"></span> Duplicate
|
|
{% elif scan.duplicate_status == '03' %}
|
|
<span class="status-dot status-dot-orange"></span> Dup (Other Loc)
|
|
{% elif row_class == 'weight_discrepancy' %}
|
|
<span class="status-dot status-dot-orange"></span> Weight Off
|
|
{% elif scan.master_status == 'match' %}
|
|
<span class="status-dot status-dot-green"></span> Match
|
|
{% elif scan.master_status == 'wrong_location' %}
|
|
<span class="status-dot status-dot-yellow"></span> Wrong Loc
|
|
{% elif scan.master_status == 'ghost_lot' %}
|
|
<span class="status-dot status-dot-purple"></span> Ghost
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if session_type == 'cycle_count' and expected_lots %}
|
|
<div class="expected-section" id="expectedSection">
|
|
<div class="scans-header">
|
|
<h3 class="scans-title expected-title">Expected Items (<span id="expectedCount">{{ expected_lots|length }}</span>)</h3>
|
|
</div>
|
|
<div class="scans-grid" id="expectedList">
|
|
{% for lot in expected_lots %}
|
|
<div class="scan-row expected-row"
|
|
id="expected-{{ lot.lot_number }}"
|
|
onclick="scanExpectedLot('{{ lot.lot_number }}')">
|
|
<div class="scan-row-lot">{{ lot.lot_number }}</div>
|
|
<div class="scan-row-item">{{ lot.item }}</div>
|
|
<div class="scan-row-weight">{{ lot.total_weight }} lbs</div>
|
|
<div class="scan-row-status">
|
|
<span class="status-dot status-dot-gray"></span> Pending
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
<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>
|
|
|
|
<div class="finish-section">
|
|
<div class="action-buttons-row">
|
|
<a href="{{ url_for('counting.my_counts', session_id=session_id) }}" class="btn btn-secondary btn-block btn-lg">
|
|
← Back to My Counts
|
|
</a>
|
|
<button id="finishBtn" class="btn btn-success btn-block btn-lg" onclick="finishLocation()">
|
|
✓ Finish Location
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentLotNumber = '';
|
|
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("counting.scan_lot", session_id=session_id, location_count_id=location.location_count_id) }}', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
lot_number: currentLotNumber,
|
|
weight: 0,
|
|
check_only: true
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.needs_confirmation) {
|
|
document.getElementById('duplicateLotNumber').textContent = currentLotNumber;
|
|
document.getElementById('duplicateModal').style.display = 'flex';
|
|
} else {
|
|
showWeightModal();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error checking duplicate:', error);
|
|
showWeightModal();
|
|
});
|
|
}
|
|
|
|
function confirmDuplicate() {
|
|
isDuplicateConfirmed = true;
|
|
document.getElementById('duplicateModal').style.display = 'none';
|
|
showWeightModal();
|
|
}
|
|
|
|
function cancelDuplicate() {
|
|
document.getElementById('duplicateModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
isDuplicateConfirmed = false;
|
|
isProcessing = false;
|
|
}
|
|
|
|
function showWeightModal() {
|
|
if (!currentLotNumber) {
|
|
alert('Error: Lot number lost. Please scan again.');
|
|
document.getElementById('lotInput').focus();
|
|
return;
|
|
}
|
|
document.getElementById('modalLotNumber').textContent = currentLotNumber;
|
|
document.getElementById('modalItemDesc').textContent = 'Loading...';
|
|
document.getElementById('weightModal').style.display = 'flex';
|
|
document.getElementById('weightInput').value = '';
|
|
document.getElementById('weightInput').focus();
|
|
}
|
|
|
|
// Weight form handler
|
|
document.getElementById('weightForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const weight = document.getElementById('weightInput').value;
|
|
if (!weight || weight <= 0) {
|
|
alert('Please enter a valid weight');
|
|
return;
|
|
}
|
|
submitScan(weight);
|
|
});
|
|
|
|
function submitScan(weight) {
|
|
if (!currentLotNumber) {
|
|
alert('Error: Lot number lost. Please scan again.');
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
return;
|
|
}
|
|
|
|
fetch('{{ url_for("counting.scan_lot", session_id=session_id, location_count_id=location.location_count_id) }}', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
lot_number: currentLotNumber.trim(),
|
|
weight: parseFloat(weight),
|
|
confirm_duplicate: isDuplicateConfirmed
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
isDuplicateConfirmed = false;
|
|
|
|
if (data.updated_entry_ids && data.updated_entry_ids.length > 0) {
|
|
updateExistingScansStatus(data.updated_entry_ids, data.duplicate_status);
|
|
}
|
|
|
|
addScanToList(data, weight);
|
|
|
|
// NEW: Instacart Logic - Remove from Expected List if it exists
|
|
const expectedRow = document.getElementById('expected-' + currentLotNumber);
|
|
if (expectedRow) {
|
|
expectedRow.remove();
|
|
|
|
// Update Expected count
|
|
const countSpan = document.getElementById('expectedCount');
|
|
if (countSpan) {
|
|
let currentCount = parseInt(countSpan.textContent);
|
|
if (currentCount > 0) {
|
|
countSpan.textContent = currentCount - 1;
|
|
}
|
|
// Hide section if empty
|
|
if (currentCount - 1 <= 0) {
|
|
const section = document.getElementById('expectedSection');
|
|
if (section) section.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
currentLotNumber = '';
|
|
isProcessing = false;
|
|
document.getElementById('lotInput').focus();
|
|
} else {
|
|
alert(data.message || 'Error saving scan');
|
|
isProcessing = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
alert('Error saving scan');
|
|
console.error(error);
|
|
isProcessing = false;
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// Function to handle clicking an Expected Lot
|
|
function scanExpectedLot(lotNumber) {
|
|
if (isProcessing) return;
|
|
|
|
// Set the lot number as if it was scanned
|
|
currentLotNumber = lotNumber;
|
|
|
|
// Visual feedback (optional, but nice)
|
|
console.log('Clicked expected lot:', lotNumber);
|
|
|
|
// Proceed to check logic (just like a normal scan)
|
|
// We go through checkDuplicate just in case, consistency is safer
|
|
checkDuplicate();
|
|
}
|
|
|
|
|
|
|
|
function cancelWeight() {
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
isDuplicateConfirmed = false;
|
|
isProcessing = false;
|
|
}
|
|
|
|
function updateExistingScansStatus(entryIds, duplicateStatus) {
|
|
entryIds.forEach(entryId => {
|
|
const scanRow = document.querySelector(`[data-entry-id="${entryId}"]`);
|
|
if (scanRow) {
|
|
const statusElement = scanRow.querySelector('.scan-row-status');
|
|
if (statusElement) {
|
|
let statusText = 'Duplicate';
|
|
let statusDot = 'blue';
|
|
if (duplicateStatus === '03') {
|
|
statusText = 'Dup (Other Loc)';
|
|
statusDot = 'orange';
|
|
}
|
|
statusElement.innerHTML = `<span class="status-dot status-dot-${statusDot}"></span> ${statusText}`;
|
|
}
|
|
scanRow.className = 'scan-row scan-row-duplicate-' + duplicateStatus;
|
|
}
|
|
});
|
|
}
|
|
|
|
function addScanToList(data, weight) {
|
|
const scansList = document.getElementById('scansList');
|
|
|
|
let statusClass = '';
|
|
let statusText = '';
|
|
let statusDot = '';
|
|
|
|
if (data.duplicate_status && data.duplicate_status !== '00') {
|
|
statusClass = 'duplicate-' + data.duplicate_status;
|
|
if (data.duplicate_status === '01' || data.duplicate_status === '04') {
|
|
statusText = 'Duplicate';
|
|
statusDot = 'blue';
|
|
} else if (data.duplicate_status === '03') {
|
|
statusText = 'Dup (Other Loc)';
|
|
statusDot = 'orange';
|
|
}
|
|
} else if (data.master_status === 'match') {
|
|
if (data.master_expected_weight && Math.abs(weight - data.master_expected_weight) >= 0.01) {
|
|
statusClass = 'weight_discrepancy';
|
|
statusText = 'Weight Off';
|
|
statusDot = 'orange';
|
|
} else {
|
|
statusClass = 'match';
|
|
statusText = 'Match';
|
|
statusDot = 'green';
|
|
}
|
|
} else if (data.master_status === 'wrong_location') {
|
|
statusClass = 'wrong_location';
|
|
statusText = 'Wrong Loc';
|
|
statusDot = 'yellow';
|
|
} else if (data.master_status === 'ghost_lot') {
|
|
statusClass = 'ghost_lot';
|
|
statusText = 'Ghost';
|
|
statusDot = 'purple';
|
|
}
|
|
|
|
const scanRow = document.createElement('div');
|
|
scanRow.className = 'scan-row scan-row-' + statusClass;
|
|
scanRow.setAttribute('data-entry-id', data.entry_id);
|
|
scanRow.onclick = function() { openScanDetail(data.entry_id); };
|
|
scanRow.innerHTML = `
|
|
<div class="scan-row-lot">${currentLotNumber}</div>
|
|
<div class="scan-row-item">${data.item || 'N/A'}</div>
|
|
<div class="scan-row-weight">${weight} lbs</div>
|
|
<div class="scan-row-status">
|
|
<span class="status-dot status-dot-${statusDot}"></span> ${statusText}
|
|
</div>
|
|
`;
|
|
|
|
scansList.insertBefore(scanRow, scansList.firstChild);
|
|
|
|
// Update Scanned Lots count
|
|
const countSpan = document.getElementById('scanListCount');
|
|
if (countSpan) {
|
|
countSpan.textContent = scansList.children.length;
|
|
}
|
|
|
|
// Update Header Found Count
|
|
const foundStat = document.getElementById('foundCount');
|
|
if (foundStat) {
|
|
foundStat.textContent = scansList.children.length;
|
|
}
|
|
}
|
|
|
|
function openScanDetail(entryId) {
|
|
fetch('/scan/' + entryId + '/details').then(r => r.json()).then(data => {
|
|
if (data.success) displayScanDetail(data.scan);
|
|
else alert('Error loading scan details');
|
|
});
|
|
}
|
|
|
|
function displayScanDetail(scan) {
|
|
const content = document.getElementById('scanDetailContent');
|
|
|
|
// LOGIC FIX: Check weight variance for the badge
|
|
let statusBadge = '';
|
|
|
|
// Check for weight discrepancy (Tolerance 0.01)
|
|
let isWeightOff = false;
|
|
if (scan.master_status === 'match' && scan.master_expected_weight) {
|
|
if (Math.abs(scan.actual_weight - scan.master_expected_weight) >= 0.01) {
|
|
isWeightOff = true;
|
|
}
|
|
}
|
|
|
|
if (scan.duplicate_status === '01' || scan.duplicate_status === '04') {
|
|
statusBadge = '<span class="badge badge-duplicate">🔵 Duplicate</span>';
|
|
} else if (scan.duplicate_status === '03') {
|
|
statusBadge = '<span class="badge badge-orange">🟠 Duplicate (Other Location)</span>';
|
|
} else if (isWeightOff) {
|
|
// NEW BADGE FOR WEIGHT OFF
|
|
statusBadge = '<span class="badge badge-warning">🟠 Weight Discrepancy</span>';
|
|
} else if (scan.master_status === 'match') {
|
|
statusBadge = '<span class="badge badge-success">✓ Match</span>';
|
|
} else if (scan.master_status === 'wrong_location') {
|
|
statusBadge = '<span class="badge badge-warning">⚠ Wrong Location</span>';
|
|
} else if (scan.master_status === 'ghost_lot') {
|
|
statusBadge = '<span class="badge badge-purple">🟣 Ghost Lot</span>';
|
|
}
|
|
|
|
// ... (rest of the function continues as normal)
|
|
|
|
content.innerHTML = `
|
|
<div class="detail-section">
|
|
<div class="detail-row">
|
|
<span class="detail-label">Lot Number:</span>
|
|
<span class="detail-value detail-lot">${scan.lot_number}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Description:</span>
|
|
<span class="detail-value">${scan.description || 'N/A'}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Status:</span>
|
|
<span class="detail-value">${statusBadge}</span>
|
|
</div>
|
|
${scan.duplicate_info ? `<div class="detail-row"><span class="detail-label">Info:</span><span class="detail-value detail-info">${scan.duplicate_info}</span></div>` : ''}
|
|
<div class="detail-row">
|
|
<span class="detail-label">Scanned:</span>
|
|
<span class="detail-value">${scan.scan_timestamp}</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="${scan.item || ''}" placeholder="Item/SKU">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Weight (lbs)</label>
|
|
<input type="number" id="editWeight" class="form-input" value="${scan.actual_weight}" step="0.01" min="0" inputmode="decimal">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Comment</label>
|
|
<textarea id="editComment" class="form-textarea" rows="3" placeholder="Add a comment...">${scan.comment || ''}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="detail-actions">
|
|
<button class="btn btn-secondary" onclick="closeScanDetail()">Cancel</button>
|
|
<button class="btn btn-danger" onclick="deleteFromDetail(${scan.entry_id})">Delete</button>
|
|
<button class="btn btn-primary" onclick="saveScanEdit(${scan.entry_id})">Save Changes</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('scanDetailModal').style.display = 'flex';
|
|
}
|
|
|
|
function closeScanDetail() {
|
|
document.getElementById('scanDetailModal').style.display = 'none';
|
|
}
|
|
|
|
function saveScanEdit(entryId) {
|
|
const item = document.getElementById('editItem').value.trim();
|
|
const weight = document.getElementById('editWeight').value;
|
|
const comment = document.getElementById('editComment').value;
|
|
|
|
if (!weight || weight <= 0) {
|
|
alert('Please enter a valid weight');
|
|
return;
|
|
}
|
|
|
|
fetch('/scan/' + entryId + '/update', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ item: item, weight: parseFloat(weight), comment: comment })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
closeScanDetail();
|
|
location.reload();
|
|
} else {
|
|
alert(data.message || 'Error updating scan');
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteFromDetail(entryId) {
|
|
if (!confirm('Delete this scan?')) return;
|
|
|
|
fetch('/scan/' + entryId + '/delete', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'}
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
closeScanDetail();
|
|
location.reload();
|
|
} else {
|
|
alert(data.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
function finishLocation() {
|
|
if (!confirm('Are you finished counting this location?')) return;
|
|
|
|
fetch('{{ url_for("counting.finish_location", session_id=session_id, location_count_id=location.location_count_id) }}', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'}
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) window.location.href = data.redirect;
|
|
else alert('Error finishing location');
|
|
});
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
document.getElementById('weightModal').style.display = 'none';
|
|
document.getElementById('duplicateModal').style.display = 'none';
|
|
document.getElementById('lotInput').focus();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |