v0.14.0 - Major Logic Overhaul & Real-Time Dashboard
Logic: Implemented "One User, One Bin" locking to prevent duplicate counting.
Integrity: Standardized is_deleted = 0 and tightened "Matched" criteria to require zero weight variance.
Refresh: Added silent 30-second dashboard polling for all 6 status categories and active counter list.
Tracking: Built user-specific activity tracking to identify who is counting where in real-time.
Stability: Resolved persistent 500 errors by finalizing the active-counters-fragment structure.
This commit is contained in:
@@ -163,9 +163,7 @@
|
||||
<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>
|
||||
{# Finish button moved to Admin Dashboard #}
|
||||
</div>
|
||||
</div>
|
||||
<button class="scroll-to-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
|
||||
|
||||
@@ -49,14 +49,6 @@
|
||||
<a href="{{ url_for('counting.count_location', session_id=count_session.session_id, location_count_id=bin.location_count_id) }}" class="btn btn-primary btn-block">
|
||||
Resume Counting
|
||||
</a>
|
||||
<div class="bin-actions-row">
|
||||
<button class="btn btn-secondary" onclick="markComplete('{{ bin.location_count_id }}')">
|
||||
✓ Mark Complete
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="deleteBinCount('{{ bin.location_count_id }}', '{{ bin.location_name }}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
12
templates/counts/partials/_active_counters.html
Normal file
12
templates/counts/partials/_active_counters.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div class="counter-list">
|
||||
{% for counter in active_counters %}
|
||||
<div class="counter-item">
|
||||
<div class="counter-avatar">{{ counter.full_name[0] }}</div>
|
||||
<div class="counter-info">
|
||||
<div class="counter-name">{{ counter.full_name }}</div>
|
||||
<div class="counter-location">📍 {{ counter.location_name }}</div>
|
||||
</div>
|
||||
<div class="counter-time">{{ counter.start_timestamp[11:16] }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -72,43 +72,43 @@
|
||||
</div>
|
||||
|
||||
<!-- Statistics Section -->
|
||||
<div class="section-card">
|
||||
<h2 class="section-title">Real-Time Statistics</h2>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-match" onclick="showStatusDetails('match')">
|
||||
<div class="stat-number">{{ stats.matched or 0 }}</div>
|
||||
<div class="stat-label">✓ Matched</div>
|
||||
</div>
|
||||
<div class="stat-card stat-duplicate" onclick="showStatusDetails('duplicates')">
|
||||
<div class="stat-number">{{ stats.duplicates or 0 }}</div>
|
||||
<div class="stat-label">🔵 Duplicates</div>
|
||||
</div>
|
||||
<div class="stat-card stat-weight-disc" onclick="showStatusDetails('weight_discrepancy')">
|
||||
<div class="stat-number">{{ stats.weight_discrepancy or 0 }}</div>
|
||||
<div class="stat-label">⚖️ Weight Discrepancy</div>
|
||||
</div>
|
||||
<div class="stat-card stat-wrong" onclick="showStatusDetails('wrong_location')">
|
||||
<div class="stat-number">{{ stats.wrong_location or 0 }}</div>
|
||||
<div class="stat-label">⚠ Wrong Location</div>
|
||||
</div>
|
||||
<div class="stat-card stat-ghost" onclick="showStatusDetails('ghost_lot')">
|
||||
<div class="stat-number">{{ stats.ghost_lots or 0 }}</div>
|
||||
<div class="stat-label">🟣 Ghost Lots</div>
|
||||
</div>
|
||||
<div class="stat-card stat-missing" onclick="showStatusDetails('missing')">
|
||||
<div class="stat-number">{{ stats.missing_lots or 0 }}</div>
|
||||
<div class="stat-label">🔴 Missing</div>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
<h2 class="section-title">Real-Time Statistics</h2>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-match" onclick="showStatusDetails('match')">
|
||||
<div class="stat-number" id="count-matched">{{ stats.matched or 0 }}</div>
|
||||
<div class="stat-label">✓ Matched</div>
|
||||
</div>
|
||||
<div class="stat-card stat-duplicate" onclick="showStatusDetails('duplicates')">
|
||||
<div class="stat-number" id="count-duplicates">{{ stats.duplicates or 0 }}</div>
|
||||
<div class="stat-label">🔵 Duplicates</div>
|
||||
</div>
|
||||
<div class="stat-card stat-weight-disc" onclick="showStatusDetails('weight_discrepancy')">
|
||||
<div class="stat-number" id="count-discrepancy">{{ stats.weight_discrepancy or 0 }}</div>
|
||||
<div class="stat-label">⚖️ Weight Discrepancy</div>
|
||||
</div>
|
||||
<div class="stat-card stat-wrong" onclick="showStatusDetails('wrong_location')">
|
||||
<div class="stat-number" id="count-wrong">{{ stats.wrong_location or 0 }}</div>
|
||||
<div class="stat-label">⚠ Wrong Location</div>
|
||||
</div>
|
||||
<div class="stat-card stat-ghost" onclick="showStatusDetails('ghost_lot')">
|
||||
<div class="stat-number" id="count-ghost">{{ stats.ghost_lots or 0 }}</div>
|
||||
<div class="stat-label">🟣 Ghost Lots</div>
|
||||
</div>
|
||||
<div class="stat-card stat-missing" onclick="showStatusDetails('missing')">
|
||||
<div class="stat-number" id="count-missing">{{ stats.missing_lots or 0 }}</div>
|
||||
<div class="stat-label">🔴 Missing</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Counters Section -->
|
||||
{% if active_counters %}
|
||||
<div class="section-card">
|
||||
<h2 class="section-title">Active Counters</h2>
|
||||
<div class="counter-list">
|
||||
{% for counter in active_counters %}
|
||||
<div id="active-counters-container"> <div class="counter-list">
|
||||
{% for counter in active_counters %}
|
||||
<div class="counter-item">
|
||||
<div class="counter-avatar">{{ counter.full_name[0] }}</div>
|
||||
<div class="counter-info">
|
||||
@@ -125,7 +125,12 @@
|
||||
<!-- Location Progress Section -->
|
||||
{% if locations %}
|
||||
<div class="section-card">
|
||||
<h2 class="section-title">Location Progress</h2>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md);">
|
||||
<h2 class="section-title" style="margin-bottom: 0;">Location Progress</h2>
|
||||
<button class="btn btn-danger btn-sm" onclick="showFinalizeAllConfirm()">
|
||||
⚠️ Finalize All Bins
|
||||
</button>
|
||||
</div>
|
||||
<div class="location-table">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -157,7 +162,7 @@
|
||||
<td>{{ loc.counter_name or '-' }}</td>
|
||||
<td>{{ loc.expected_lots_master }}</td>
|
||||
<td>{{ loc.lots_found }}</td>
|
||||
<td>{{ loc.lots_missing }}</td>
|
||||
<td>{{ loc.lots_missing_calc }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -198,6 +203,9 @@
|
||||
<button id="reopenLocationBtn" class="btn btn-warning btn-sm" style="display: none;" onclick="showReopenConfirm()">
|
||||
🔓 Reopen Location
|
||||
</button>
|
||||
<button id="deleteLocationBtn" class="btn btn-danger btn-sm" style="display: none;" onclick="showDeleteBinConfirm()">
|
||||
🗑️ Delete Bin
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="exportLocationToCSV()">
|
||||
📥 Export CSV
|
||||
</button>
|
||||
@@ -460,7 +468,10 @@ function showLocationDetails(locationCountId, locationName, status) {
|
||||
// Show finalize or reopen button based on status
|
||||
const finalizeBtn = document.getElementById('finalizeLocationBtn');
|
||||
const reopenBtn = document.getElementById('reopenLocationBtn');
|
||||
|
||||
const deleteBtn = document.getElementById('deleteLocationBtn'); // ADD THIS LINE
|
||||
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
if (status === 'in_progress') {
|
||||
finalizeBtn.style.display = 'block';
|
||||
reopenBtn.style.display = 'none';
|
||||
@@ -621,8 +632,8 @@ function closeFinalizeConfirm() {
|
||||
}
|
||||
|
||||
function confirmFinalize() {
|
||||
// Note: The /complete endpoint is handled by blueprints/counting.py
|
||||
fetch(`/location/${currentLocationId}/complete`, {
|
||||
// Correctly points to the /finish route to trigger Missing Lot calculations
|
||||
fetch(`/count/${CURRENT_SESSION_ID}/location/${currentLocationId}/finish`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -633,16 +644,18 @@ function confirmFinalize() {
|
||||
if (data.success) {
|
||||
closeFinalizeConfirm();
|
||||
closeLocationModal();
|
||||
location.reload(); // Reload to show updated status
|
||||
location.reload(); // Reload to show updated status and Missing counts
|
||||
} else {
|
||||
alert(data.message || 'Error finalizing location');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Finalize Error:', error);
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function showReopenConfirm() {
|
||||
document.getElementById('reopenBinName').textContent = currentLocationName;
|
||||
document.getElementById('reopenConfirmModal').style.display = 'flex';
|
||||
@@ -717,5 +730,78 @@ function activateSession() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showFinalizeAllConfirm() {
|
||||
if (confirm("⚠️ WARNING: This will finalize ALL open bins in this session and calculate missing items. This cannot be undone. Are you sure?")) {
|
||||
fetch(`/session/${CURRENT_SESSION_ID}/finalize-all`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert("Error: " + data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteBinConfirm() {
|
||||
if (confirm(`⚠️ DANGER: Are you sure you want to delete ALL data for ${currentLocationName}? This will hide the bin from staff and wipe any missing lot flags.`)) {
|
||||
fetch(`/location/${currentLocationId}/delete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeLocationModal();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Error deleting bin');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDashboardStats() {
|
||||
const sessionId = CURRENT_SESSION_ID;
|
||||
|
||||
fetch(`/session/${sessionId}/get_stats`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const s = data.stats;
|
||||
// These IDs must match your HTML and the keys must match sessions.py
|
||||
if (document.getElementById('count-matched')) document.getElementById('count-matched').innerText = s.matched;
|
||||
if (document.getElementById('count-duplicates')) document.getElementById('count-duplicates').innerText = s.duplicates;
|
||||
if (document.getElementById('count-discrepancy')) document.getElementById('count-discrepancy').innerText = s.discrepancy;
|
||||
if (document.getElementById('count-wrong')) document.getElementById('count-wrong').innerText = s.wrong_location; // Fixed
|
||||
if (document.getElementById('count-ghost')) document.getElementById('count-ghost').innerText = s.ghost_lots; // Fixed
|
||||
if (document.getElementById('count-missing')) document.getElementById('count-missing').innerText = s.missing;
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Error refreshing stats:', err));
|
||||
|
||||
fetch(`/session/${sessionId}/active-counters-fragment`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const container = document.getElementById('active-counters-container');
|
||||
if (container) container.innerHTML = html;
|
||||
})
|
||||
.catch(err => console.error('Error refreshing counters:', err));
|
||||
}
|
||||
|
||||
// This tells the browser: "Run the refresh function every 30 seconds"
|
||||
setInterval(refreshDashboardStats, 30000);
|
||||
|
||||
// This runs it IMMEDIATELY once so you don't wait 30 seconds for the first update
|
||||
refreshDashboardStats();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user