diff --git a/app.py b/app.py index a324ce3..d31659b 100644 --- a/app.py +++ b/app.py @@ -38,7 +38,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1) # 1. Define the version -APP_VERSION = '0.13.2' +APP_VERSION = '0.14.0' # 2. Inject it into all templates automatically @app.context_processor diff --git a/blueprints/__pycache__/admin_locations.cpython-313.pyc b/blueprints/__pycache__/admin_locations.cpython-313.pyc index 8a81d39..700ab2f 100644 Binary files a/blueprints/__pycache__/admin_locations.cpython-313.pyc and b/blueprints/__pycache__/admin_locations.cpython-313.pyc differ diff --git a/blueprints/__pycache__/sessions.cpython-313.pyc b/blueprints/__pycache__/sessions.cpython-313.pyc index aa3fcec..e9a6c84 100644 Binary files a/blueprints/__pycache__/sessions.cpython-313.pyc and b/blueprints/__pycache__/sessions.cpython-313.pyc differ diff --git a/blueprints/admin_locations.py b/blueprints/admin_locations.py index 5a96514..8334a68 100644 --- a/blueprints/admin_locations.py +++ b/blueprints/admin_locations.py @@ -29,35 +29,6 @@ def reopen_location(location_count_id): return jsonify({'success': True, 'message': 'Bin reopened for counting'}) -@admin_locations_bp.route('/location//delete', methods=['POST']) -@login_required -def delete_location_count(location_count_id): - """Delete all counts for a location (soft delete)""" - # Verify ownership - loc = query_db('SELECT * FROM LocationCounts WHERE location_count_id = ?', [location_count_id], one=True) - - if not loc: - return jsonify({'success': False, 'message': 'Location not found'}) - - if loc['counted_by'] != session['user_id'] and session['role'] not in ['owner', 'admin']: - return jsonify({'success': False, 'message': 'Permission denied'}) - - # Soft delete all scan entries for this location - execute_db(''' - UPDATE ScanEntries - SET is_deleted = 1 - WHERE location_count_id = ? - ''', [location_count_id]) - - # Delete the location count record - execute_db(''' - DELETE FROM LocationCounts - WHERE location_count_id = ? - ''', [location_count_id]) - - return jsonify({'success': True, 'message': 'Bin count deleted'}) - - @admin_locations_bp.route('/location//scans') @login_required def get_location_scans(location_count_id): @@ -86,4 +57,40 @@ def get_location_scans(location_count_id): return jsonify({'success': True, 'scans': scans_list}) except Exception as e: - return jsonify({'success': False, 'message': str(e)}) \ No newline at end of file + return jsonify({'success': False, 'message': str(e)}) + +@admin_locations_bp.route('/location//delete', methods=['POST']) +@login_required +def soft_delete_location(location_count_id): + """Admin-only: Soft delete a bin count and its associated data""" + if session.get('role') not in ['owner', 'admin']: + return jsonify({'success': False, 'message': 'Admin role required'}), 403 + + # 1. Verify location exists + loc = query_db('SELECT session_id, location_name FROM LocationCounts WHERE location_count_id = ?', + [location_count_id], one=True) + + if not loc: + return jsonify({'success': False, 'message': 'Location not found'}) + + # 2. Soft delete the bin count itself + execute_db(''' + UPDATE LocationCounts + SET is_deleted = 1 + WHERE location_count_id = ? + ''', [location_count_id]) + + # 3. Soft delete all scans in that bin + execute_db(''' + UPDATE ScanEntries + SET is_deleted = 1 + WHERE location_count_id = ? + ''', [location_count_id]) + + # 4. Remove any MissingLots records generated for this bin + execute_db(''' + DELETE FROM MissingLots + WHERE session_id = ? AND master_expected_location = ? + ''', [loc['session_id'], loc['location_name']]) + + return jsonify({'success': True, 'message': 'Bin count and associated data soft-deleted'}) \ No newline at end of file diff --git a/blueprints/counting.py b/blueprints/counting.py index 4a5ee4a..b584216 100644 --- a/blueprints/counting.py +++ b/blueprints/counting.py @@ -111,16 +111,23 @@ def my_counts(session_id): # Get this user's active bins active_bins = query_db(''' - SELECT lc.*, - COUNT(se.entry_id) as scan_count - FROM LocationCounts lc - LEFT JOIN ScanEntries se ON lc.location_count_id = se.location_count_id AND se.is_deleted = 0 - WHERE lc.session_id = ? - AND lc.counted_by = ? - AND lc.status = 'in_progress' - GROUP BY lc.location_count_id - ORDER BY lc.start_timestamp DESC - ''', [session_id, session['user_id']]) + SELECT lc.*, + COUNT(se.entry_id) as scan_count + FROM LocationCounts lc + LEFT JOIN ScanEntries se ON lc.location_count_id = se.location_count_id AND se.is_deleted = 0 + WHERE lc.session_id = ? + AND lc.status = 'in_progress' + AND lc.is_deleted = 0 + AND ( + lc.counted_by = ? + OR lc.location_count_id IN ( + SELECT location_count_id FROM ScanEntries + WHERE scanned_by = ? AND is_deleted = 0 + ) + ) + GROUP BY lc.location_count_id + ORDER BY lc.start_timestamp DESC +''', [session_id, session['user_id'], session['user_id']]) # Get this user's completed bins completed_bins = query_db(''' @@ -129,11 +136,17 @@ def my_counts(session_id): FROM LocationCounts lc LEFT JOIN ScanEntries se ON lc.location_count_id = se.location_count_id AND se.is_deleted = 0 WHERE lc.session_id = ? - AND lc.counted_by = ? AND lc.status = 'completed' + AND ( + lc.counted_by = ? + OR lc.location_count_id IN ( + SELECT location_count_id FROM ScanEntries + WHERE scanned_by = ? AND is_deleted = 0 + ) + ) GROUP BY lc.location_count_id - ORDER BY lc.end_timestamp DESC - ''', [session_id, session['user_id']]) + ORDER BY lc.start_timestamp DESC + ''', [session_id, session['user_id'], session['user_id']]) return render_template('counts/my_counts.html', count_session=sess, @@ -144,7 +157,7 @@ def my_counts(session_id): @counting_bp.route('/session//start-bin', methods=['POST']) @login_required def start_bin_count(session_id): - """Start counting a new bin""" + """Start counting a new bin or resume an existing in-progress one""" sess = get_active_session(session_id) if not sess: flash('Session not found or archived', 'warning') @@ -158,7 +171,21 @@ def start_bin_count(session_id): if not location_name: flash('Bin number is required', 'danger') return redirect(url_for('counting.my_counts', session_id=session_id)) - + + # --- NEW LOGIC: Check for existing in-progress bin --- + existing_bin = query_db(''' + SELECT location_count_id + FROM LocationCounts + WHERE session_id = ? AND location_name = ? AND status = 'in_progress' + ''', [session_id, location_name], one=True) + + if existing_bin: + flash(f'Resuming bin: {location_name}', 'info') + return redirect(url_for('counting.count_location', + session_id=session_id, + location_count_id=existing_bin['location_count_id'])) + # --- END NEW LOGIC --- + # Count expected lots from MASTER baseline for this location expected_lots = query_db(''' SELECT COUNT(DISTINCT lot_number) as count @@ -168,7 +195,7 @@ def start_bin_count(session_id): expected_count = expected_lots['count'] if expected_lots else 0 - # Create new location count + # Create new location count if none existed conn = get_db() cursor = conn.cursor() @@ -184,7 +211,6 @@ def start_bin_count(session_id): flash(f'Started counting bin: {location_name}', 'success') return redirect(url_for('counting.count_location', session_id=session_id, location_count_id=location_count_id)) - @counting_bp.route('/location//complete', methods=['POST']) @login_required def complete_location(location_count_id): @@ -512,7 +538,7 @@ def scan_lot(session_id, location_count_id): def delete_scan(entry_id): """Soft delete a scan and recalculate duplicate statuses""" # Get the scan being deleted - scan = query_db('SELECT * FROM ScanEntries WHERE entry_id = ?', [entry_id], one=True) + scan = query_db('SELECT * FROM ScanEntries WHERE entry_id = ? AND is_deleted = 0', [entry_id], one=True) if not scan: return jsonify({'success': False, 'message': 'Scan not found'}) @@ -572,7 +598,7 @@ def update_scan(entry_id): comment = data.get('comment', '') # Get the scan - scan = query_db('SELECT * FROM ScanEntries WHERE entry_id = ?', [entry_id], one=True) + scan = query_db('SELECT * FROM ScanEntries WHERE entry_id = ? AND is_deleted = 0', [entry_id], one=True) if not scan: return jsonify({'success': False, 'message': 'Scan not found'}) @@ -593,7 +619,7 @@ def update_scan(entry_id): actual_weight = ?, comment = ?, modified_timestamp = CURRENT_TIMESTAMP - WHERE entry_id = ? + WHERE entry_id = ? and is_deleted = 0 ''', [item, weight, comment, entry_id]) return jsonify({'success': True, 'message': 'Scan updated'}) @@ -625,7 +651,7 @@ def recalculate_duplicate_status(session_id, lot_number, current_location): duplicate_info = NULL, comment = NULL, modified_timestamp = CURRENT_TIMESTAMP - WHERE entry_id = ? + WHERE entry_id = ? and is_deleted = 0 ''', [scan['entry_id']]) updated_entries.append({ 'entry_id': scan['entry_id'], @@ -670,7 +696,7 @@ def recalculate_duplicate_status(session_id, lot_number, current_location): duplicate_info = ?, comment = ?, modified_timestamp = CURRENT_TIMESTAMP - WHERE entry_id = ? + WHERE entry_id = ? and is_deleted = 0 ''', [duplicate_status, duplicate_info, duplicate_info, scan['entry_id']]) # Update our tracking list @@ -689,7 +715,7 @@ def recalculate_duplicate_status(session_id, lot_number, current_location): duplicate_info = ?, comment = ?, modified_timestamp = CURRENT_TIMESTAMP - WHERE entry_id = ? + WHERE entry_id = ? and is_deleted = 0 ''', [duplicate_status, duplicate_info, duplicate_info, prev_scan['entry_id']]) # Update tracking for previous scans @@ -754,4 +780,57 @@ def finish_location(session_id, location_count_id): return jsonify({ 'success': True, 'redirect': url_for('counting.count_session', session_id=session_id) - }) \ No newline at end of file + }) + +@counting_bp.route('/session//finalize-all', methods=['POST']) +@login_required +def finalize_all_locations(session_id): + """Finalize all 'in_progress' locations in a session""" + if session.get('role') not in ['owner', 'admin']: + return jsonify({'success': False, 'message': 'Permission denied'}), 403 + + # 1. Get all in_progress locations for this session + locations = query_db(''' + SELECT location_count_id, location_name + FROM LocationCounts + WHERE session_id = ? + AND status = 'in_progress' + AND is_deleted = 0 +''', [session_id]) + + if not locations: + return jsonify({'success': True, 'message': 'No open bins to finalize.'}) + + # 2. Loop through and run the finalize logic for each + for loc in locations: + # We reuse the logic from your existing finish_location route + execute_db(''' + UPDATE LocationCounts + SET status = 'completed', end_timestamp = CURRENT_TIMESTAMP + WHERE location_count_id = ? + ''', [loc['location_count_id']]) + + # Identify missing lots from MASTER baseline + expected_lots = query_db(''' + SELECT lot_number, item, description, system_quantity + FROM BaselineInventory_Master + WHERE session_id = ? AND system_bin = ? + ''', [session_id, loc['location_name']]) + + scanned_lots = query_db(''' + SELECT DISTINCT lot_number + FROM ScanEntries + WHERE location_count_id = ? AND is_deleted = 0 + ''', [loc['location_count_id']]) + + scanned_lot_numbers = {s['lot_number'] for s in scanned_lots} + + for expected in expected_lots: + if expected['lot_number'] not in scanned_lot_numbers: + execute_db(''' + INSERT INTO MissingLots (session_id, lot_number, master_expected_location, item, master_expected_quantity, marked_by) + VALUES (?, ?, ?, ?, ?, ?) + ''', [session_id, expected['lot_number'], loc['location_name'], + expected['item'], expected['system_quantity'], session['user_id']]) + + return jsonify({'success': True, 'message': f'Successfully finalized {len(locations)} bins.'}) \ No newline at end of file diff --git a/blueprints/sessions.py b/blueprints/sessions.py index 8c70b69..9025048 100644 --- a/blueprints/sessions.py +++ b/blueprints/sessions.py @@ -24,7 +24,7 @@ def create_session(): flash(f'Session "{session_name}" created successfully!', 'success') return redirect(url_for('sessions.session_detail', session_id=session_id)) - return render_template('create_session.html') + return render_template('/counts/create_session.html') @sessions_bp.route('/session/') @@ -54,24 +54,38 @@ def session_detail(session_id): ''', [session_id], one=True) # Get location progress + # We add a subquery to count the actual missing lots for each bin locations = query_db(''' - SELECT lc.*, u.full_name as counter_name + SELECT + lc.*, + u.full_name as counter_name, + (SELECT COUNT(*) FROM MissingLots ml + WHERE ml.session_id = lc.session_id + AND ml.master_expected_location = lc.location_name) as lots_missing_calc FROM LocationCounts lc LEFT JOIN Users u ON lc.counted_by = u.user_id WHERE lc.session_id = ? + AND lc.is_deleted = 0 ORDER BY lc.status DESC, lc.location_name ''', [session_id]) # Get active counters active_counters = query_db(''' - SELECT DISTINCT u.full_name, lc.location_name, lc.start_timestamp + SELECT + u.full_name, + u.user_id, + MAX(lc.start_timestamp) AS start_timestamp, -- Add the alias here! + lc.location_name FROM LocationCounts lc JOIN Users u ON lc.counted_by = u.user_id - WHERE lc.session_id = ? AND lc.status = 'in_progress' - ORDER BY lc.start_timestamp DESC + WHERE lc.session_id = ? + AND lc.status = 'in_progress' + AND lc.is_deleted = 0 + GROUP BY u.user_id + ORDER BY start_timestamp DESC ''', [session_id]) - return render_template('session_detail.html', + return render_template('/counts/session_detail.html', count_session=sess, stats=stats, locations=locations, @@ -98,6 +112,7 @@ def get_status_details(session_id, status): WHERE se.session_id = ? AND se.master_status = 'match' AND se.duplicate_status = '00' + AND se.master_variance_lbs = 0 AND se.is_deleted = 0 ORDER BY se.scan_timestamp DESC ''', [session_id]) @@ -184,20 +199,21 @@ def get_status_details(session_id, status): # Missing lots (in master but not scanned) items = query_db(''' SELECT - bim.lot_number, - bim.item, + ml.lot_number, + ml.item, bim.description, - bim.system_bin, - bim.system_quantity - FROM BaselineInventory_Master bim - WHERE bim.session_id = ? - AND bim.lot_number NOT IN ( - SELECT lot_number - FROM ScanEntries - WHERE session_id = ? AND is_deleted = 0 - ) - ORDER BY bim.system_bin, bim.lot_number - ''', [session_id, session_id]) + ml.master_expected_location as system_bin, + ml.master_expected_quantity as system_quantity + FROM MissingLots ml + LEFT JOIN BaselineInventory_Master bim ON + ml.lot_number = bim.lot_number AND + ml.item = bim.item AND + ml.master_expected_location = bim.system_bin AND + ml.session_id = bim.session_id + WHERE ml.session_id = ? + GROUP BY ml.lot_number, ml.item, ml.master_expected_location + ORDER BY ml.master_expected_location, ml.lot_number + ''', [session_id]) else: return jsonify({'success': False, 'message': 'Invalid status'}) @@ -241,4 +257,40 @@ def activate_session(session_id): execute_db('UPDATE CountSessions SET status = ? WHERE session_id = ?', ['active', session_id]) - return jsonify({'success': True, 'message': 'Session activated successfully'}) \ No newline at end of file + return jsonify({'success': True, 'message': 'Session activated successfully'}) + +@sessions_bp.route('/session//get_stats') +@role_required('owner', 'admin') +def get_session_stats(session_id): + stats = query_db(''' + SELECT + COUNT(DISTINCT se.entry_id) FILTER (WHERE se.master_status = 'match' AND se.duplicate_status = '00' AND se.master_variance_lbs = 0 AND se.is_deleted = 0 AND ABS(se.actual_weight - se.master_expected_weight) < 0.01) as matched, + COUNT(DISTINCT se.lot_number) FILTER (WHERE se.duplicate_status IN ('01', '03', '04') AND se.is_deleted = 0) as duplicates, + COUNT(DISTINCT se.entry_id) FILTER (WHERE se.master_status = 'match' AND se.duplicate_status = '00' AND se.is_deleted = 0 AND ABS(se.actual_weight - se.master_expected_weight) >= 0.01) as discrepancy, + COUNT(DISTINCT se.entry_id) FILTER (WHERE se.master_status = 'wrong_location' AND se.is_deleted = 0) as wrong_location, + COUNT(DISTINCT se.entry_id) FILTER (WHERE se.master_status = 'ghost_lot' AND se.is_deleted = 0) as ghost_lots, + COUNT(DISTINCT ml.missing_id) as missing + FROM CountSessions cs + LEFT JOIN ScanEntries se ON cs.session_id = se.session_id + LEFT JOIN MissingLots ml ON cs.session_id = ml.session_id + WHERE cs.session_id = ? + ''', [session_id], one=True) + + return jsonify(success=True, stats=dict(stats)) +@sessions_bp.route('/session//active-counters-fragment') +@role_required('owner', 'admin') +def active_counters_fragment(session_id): + # Use that unique-user query we just built together + active_counters = query_db(''' + SELECT + u.full_name, + MAX(lc.start_timestamp) AS start_timestamp, + lc.location_name + FROM LocationCounts lc + JOIN Users u ON lc.counted_by = u.user_id + WHERE lc.session_id = ? AND lc.status = 'in_progress' AND lc.is_deleted = 0 + GROUP BY u.user_id + ''', [session_id]) + + # This renders JUST the list part, not the whole page + return render_template('counts/partials/_active_counters.html', active_counters=active_counters) diff --git a/migrations.py b/migrations.py index e1109b8..00ad63b 100644 --- a/migrations.py +++ b/migrations.py @@ -198,6 +198,17 @@ def migration_005_add_cons_process_fields_duplicate_key(): conn.commit() conn.close() +def migration_006_add_is_deleted_to_locationcounts(): + """Add is_deleted column to LocationCounts table""" + conn = get_db() + + if table_exists('LocationCounts'): + if not column_exists('LocationCounts', 'is_deleted'): + conn.execute('ALTER TABLE LocationCounts ADD COLUMN is_deleted INTEGER DEFAULT 0') + print(" Added is_deleted column to LocationCounts") + + conn.commit() + conn.close() # List of all migrations in order MIGRATIONS = [ @@ -206,6 +217,7 @@ MIGRATIONS = [ (3, 'add_default_modules', migration_003_add_default_modules), (4, 'assign_modules_to_admins', migration_004_assign_modules_to_admins), (5, 'add_cons_process_fields_duplicate_key', migration_005_add_cons_process_fields_duplicate_key), + (6, 'add_is_deleted_to_locationcounts', migration_006_add_is_deleted_to_locationcounts), ] diff --git a/templates/counts/count_location.html b/templates/counts/count_location.html index 606f81c..b4e6481 100644 --- a/templates/counts/count_location.html +++ b/templates/counts/count_location.html @@ -163,9 +163,7 @@ ← Back to My Counts - + {# Finish button moved to Admin Dashboard #} - - {% endfor %} diff --git a/templates/counts/partials/_active_counters.html b/templates/counts/partials/_active_counters.html new file mode 100644 index 0000000..7182ef6 --- /dev/null +++ b/templates/counts/partials/_active_counters.html @@ -0,0 +1,12 @@ +
+ {% for counter in active_counters %} +
+
{{ counter.full_name[0] }}
+
+
{{ counter.full_name }}
+
📍 {{ counter.location_name }}
+
+
{{ counter.start_timestamp[11:16] }}
+
+ {% endfor %} +
\ No newline at end of file diff --git a/templates/session_detail.html b/templates/counts/session_detail.html similarity index 82% rename from templates/session_detail.html rename to templates/counts/session_detail.html index c32047f..617d703 100644 --- a/templates/session_detail.html +++ b/templates/counts/session_detail.html @@ -72,43 +72,43 @@ -
-

Real-Time Statistics

- -
-
-
{{ stats.matched or 0 }}
-
✓ Matched
-
-
-
{{ stats.duplicates or 0 }}
-
🔵 Duplicates
-
-
-
{{ stats.weight_discrepancy or 0 }}
-
⚖️ Weight Discrepancy
-
-
-
{{ stats.wrong_location or 0 }}
-
⚠ Wrong Location
-
-
-
{{ stats.ghost_lots or 0 }}
-
🟣 Ghost Lots
-
-
-
{{ stats.missing_lots or 0 }}
-
🔴 Missing
-
+
+

Real-Time Statistics

+ +
+
+
{{ stats.matched or 0 }}
+
✓ Matched
+
+
+
{{ stats.duplicates or 0 }}
+
🔵 Duplicates
+
+
+
{{ stats.weight_discrepancy or 0 }}
+
⚖️ Weight Discrepancy
+
+
+
{{ stats.wrong_location or 0 }}
+
⚠ Wrong Location
+
+
+
{{ stats.ghost_lots or 0 }}
+
🟣 Ghost Lots
+
+
+
{{ stats.missing_lots or 0 }}
+
🔴 Missing
+
{% if active_counters %}

Active Counters

-
- {% for counter in active_counters %} +
+ {% for counter in active_counters %}
{{ counter.full_name[0] }}
@@ -125,7 +125,12 @@ {% if locations %}
-

Location Progress

+
+

Location Progress

+ +
@@ -157,7 +162,7 @@ - + {% endfor %} @@ -198,6 +203,9 @@ + @@ -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(); {% endblock %} \ No newline at end of file
{{ loc.counter_name or '-' }} {{ loc.expected_lots_master }} {{ loc.lots_found }}{{ loc.lots_missing }}{{ loc.lots_missing_calc }}