diff --git a/app.py b/app.py index 9f7e5d6..fa18fa6 100644 --- a/app.py +++ b/app.py @@ -15,10 +15,12 @@ app = Flask(__name__) from db import query_db, execute_db, get_db from blueprints.data_imports import data_imports_bp from blueprints.users import users_bp +from blueprints.sessions import sessions_bp from utils import login_required, role_required app.register_blueprint(data_imports_bp) app.register_blueprint(users_bp) +app.register_blueprint(sessions_bp) # V1.0: Use environment variable for production, fallback to demo key for development app.secret_key = os.environ.get('SCANLOOK_SECRET_KEY', 'scanlook-demo-key-replace-for-production') @@ -121,215 +123,6 @@ def staff_mode(): return render_template('staff_dashboard.html', sessions=active_sessions, is_admin_mode=True) -# ==================== ROUTES: SESSION MANAGEMENT (ADMIN) ==================== - -@app.route('/session/create', methods=['GET', 'POST']) -@role_required('owner', 'admin') -def create_session(): - """Create new count session""" - if request.method == 'POST': - session_name = request.form.get('session_name', '').strip() - session_type = request.form.get('session_type') - - if not session_name: - flash('Session name is required', 'danger') - return redirect(url_for('create_session')) - - session_id = execute_db(''' - INSERT INTO CountSessions (session_name, session_type, created_by, branch) - VALUES (?, ?, ?, ?) - ''', [session_name, session_type, session['user_id'], 'Main']) - - flash(f'Session "{session_name}" created successfully!', 'success') - return redirect(url_for('session_detail', session_id=session_id)) - - return render_template('create_session.html') - - -@app.route('/session/') -@role_required('owner', 'admin') -def session_detail(session_id): - """Session detail and monitoring page""" - sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True) - - if not sess: - flash('Session not found', 'danger') - return redirect(url_for('dashboard')) - - # Get statistics - stats = query_db(''' - SELECT - COUNT(DISTINCT se.entry_id) FILTER (WHERE se.is_deleted = 0) as total_scans, - 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 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 weight_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_lots - 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) - - # Get location progress - locations = query_db(''' - SELECT lc.*, u.full_name as counter_name - FROM LocationCounts lc - LEFT JOIN Users u ON lc.counted_by = u.user_id - WHERE lc.session_id = ? - 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 - 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 - ''', [session_id]) - - return render_template('session_detail.html', - count_session=sess, - stats=stats, - locations=locations, - active_counters=active_counters) - - -@app.route('/session//status-details/') -@role_required('owner', 'admin') -def get_status_details(session_id, status): - """Get detailed breakdown for a specific status""" - - try: - if status == 'match': - # Matched lots (not duplicates) - JOIN with CURRENT for live data - items = query_db(''' - SELECT - se.*, - u.full_name as scanned_by_name, - bic.system_bin as current_system_location, - bic.system_quantity as current_system_weight - FROM ScanEntries se - JOIN Users u ON se.scanned_by = u.user_id - LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number - WHERE se.session_id = ? - AND se.master_status = 'match' - AND se.duplicate_status = '00' - AND se.is_deleted = 0 - ORDER BY se.scan_timestamp DESC - ''', [session_id]) - - elif status == 'duplicates': - # Duplicate lots (grouped by lot number) - JOIN with CURRENT - items = query_db(''' - SELECT - se.lot_number, - se.item, - se.description, - GROUP_CONCAT(DISTINCT se.scanned_location) as scanned_location, - SUM(se.actual_weight) as actual_weight, - se.master_expected_location, - se.master_expected_weight, - GROUP_CONCAT(DISTINCT u.full_name) as scanned_by_name, - MIN(se.scan_timestamp) as scan_timestamp, - bic.system_bin as current_system_location, - bic.system_quantity as current_system_weight - FROM ScanEntries se - JOIN Users u ON se.scanned_by = u.user_id - LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number - WHERE se.session_id = ? - AND se.duplicate_status IN ('01', '03', '04') - AND se.is_deleted = 0 - GROUP BY se.lot_number - ORDER BY se.lot_number - ''', [session_id]) - - elif status == 'wrong_location': - # Wrong location lots - JOIN with CURRENT - items = query_db(''' - SELECT - se.*, - u.full_name as scanned_by_name, - bic.system_bin as current_system_location, - bic.system_quantity as current_system_weight - FROM ScanEntries se - JOIN Users u ON se.scanned_by = u.user_id - LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number - WHERE se.session_id = ? - AND se.master_status = 'wrong_location' - AND se.is_deleted = 0 - ORDER BY se.scan_timestamp DESC - ''', [session_id]) - - elif status == 'weight_discrepancy': - # Weight discrepancies (right location, wrong weight) - JOIN with CURRENT - items = query_db(''' - SELECT - se.*, - u.full_name as scanned_by_name, - bic.system_bin as current_system_location, - bic.system_quantity as current_system_weight - FROM ScanEntries se - JOIN Users u ON se.scanned_by = u.user_id - LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number - WHERE se.session_id = ? - AND se.master_status = 'match' - AND se.duplicate_status = '00' - AND ABS(se.actual_weight - se.master_expected_weight) >= 0.01 - AND se.is_deleted = 0 - ORDER BY ABS(se.actual_weight - se.master_expected_weight) DESC - ''', [session_id]) - - elif status == 'ghost_lot': - # Ghost lots (not in master baseline) - JOIN with CURRENT - items = query_db(''' - SELECT - se.*, - u.full_name as scanned_by_name, - bic.system_bin as current_system_location, - bic.system_quantity as current_system_weight - FROM ScanEntries se - JOIN Users u ON se.scanned_by = u.user_id - LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number - WHERE se.session_id = ? - AND se.master_status = 'ghost_lot' - AND se.is_deleted = 0 - ORDER BY se.scan_timestamp DESC - ''', [session_id]) - - elif status == 'missing': - # Missing lots (in master but not scanned) - items = query_db(''' - SELECT - bim.lot_number, - bim.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]) - else: - return jsonify({'success': False, 'message': 'Invalid status'}) - - return jsonify({ - 'success': True, - 'items': [dict(item) for item in items] if items else [] - }) - - except Exception as e: - print(f"Error in get_status_details: {str(e)}") - return jsonify({'success': False, 'message': f'Error: {str(e)}'}) - - # ==================== ROUTES: COUNTING (STAFF) ==================== @app.route('/count/') diff --git a/blueprints/__pycache__/sessions.cpython-313.pyc b/blueprints/__pycache__/sessions.cpython-313.pyc new file mode 100644 index 0000000..d9aa405 Binary files /dev/null and b/blueprints/__pycache__/sessions.cpython-313.pyc differ diff --git a/blueprints/sessions.py b/blueprints/sessions.py new file mode 100644 index 0000000..6e6a7c6 --- /dev/null +++ b/blueprints/sessions.py @@ -0,0 +1,211 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session +from db import query_db, execute_db +from utils import role_required + +sessions_bp = Blueprint('sessions', __name__) + +@sessions_bp.route('/session/create', methods=['GET', 'POST']) +@role_required('owner', 'admin') +def create_session(): + """Create new count session""" + if request.method == 'POST': + session_name = request.form.get('session_name', '').strip() + session_type = request.form.get('session_type') + + if not session_name: + flash('Session name is required', 'danger') + return redirect(url_for('sessions.create_session')) + + session_id = execute_db(''' + INSERT INTO CountSessions (session_name, session_type, created_by, branch) + VALUES (?, ?, ?, ?) + ''', [session_name, session_type, session['user_id'], 'Main']) + + 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') + + +@sessions_bp.route('/session/') +@role_required('owner', 'admin') +def session_detail(session_id): + """Session detail and monitoring page""" + sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True) + + if not sess: + flash('Session not found', 'danger') + return redirect(url_for('dashboard')) + + # Get statistics + stats = query_db(''' + SELECT + COUNT(DISTINCT se.entry_id) FILTER (WHERE se.is_deleted = 0) as total_scans, + 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 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 weight_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_lots + 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) + + # Get location progress + locations = query_db(''' + SELECT lc.*, u.full_name as counter_name + FROM LocationCounts lc + LEFT JOIN Users u ON lc.counted_by = u.user_id + WHERE lc.session_id = ? + 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 + 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 + ''', [session_id]) + + return render_template('session_detail.html', + count_session=sess, + stats=stats, + locations=locations, + active_counters=active_counters) + + +@sessions_bp.route('/session//status-details/') +@role_required('owner', 'admin') +def get_status_details(session_id, status): + """Get detailed breakdown for a specific status""" + + try: + if status == 'match': + # Matched lots (not duplicates) - JOIN with CURRENT for live data + items = query_db(''' + SELECT + se.*, + u.full_name as scanned_by_name, + bic.system_bin as current_system_location, + bic.system_quantity as current_system_weight + FROM ScanEntries se + JOIN Users u ON se.scanned_by = u.user_id + LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number + WHERE se.session_id = ? + AND se.master_status = 'match' + AND se.duplicate_status = '00' + AND se.is_deleted = 0 + ORDER BY se.scan_timestamp DESC + ''', [session_id]) + + elif status == 'duplicates': + # Duplicate lots (grouped by lot number) - JOIN with CURRENT + items = query_db(''' + SELECT + se.lot_number, + se.item, + se.description, + GROUP_CONCAT(DISTINCT se.scanned_location) as scanned_location, + SUM(se.actual_weight) as actual_weight, + se.master_expected_location, + se.master_expected_weight, + GROUP_CONCAT(DISTINCT u.full_name) as scanned_by_name, + MIN(se.scan_timestamp) as scan_timestamp, + bic.system_bin as current_system_location, + bic.system_quantity as current_system_weight + FROM ScanEntries se + JOIN Users u ON se.scanned_by = u.user_id + LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number + WHERE se.session_id = ? + AND se.duplicate_status IN ('01', '03', '04') + AND se.is_deleted = 0 + GROUP BY se.lot_number + ORDER BY se.lot_number + ''', [session_id]) + + elif status == 'wrong_location': + # Wrong location lots - JOIN with CURRENT + items = query_db(''' + SELECT + se.*, + u.full_name as scanned_by_name, + bic.system_bin as current_system_location, + bic.system_quantity as current_system_weight + FROM ScanEntries se + JOIN Users u ON se.scanned_by = u.user_id + LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number + WHERE se.session_id = ? + AND se.master_status = 'wrong_location' + AND se.is_deleted = 0 + ORDER BY se.scan_timestamp DESC + ''', [session_id]) + + elif status == 'weight_discrepancy': + # Weight discrepancies (right location, wrong weight) - JOIN with CURRENT + items = query_db(''' + SELECT + se.*, + u.full_name as scanned_by_name, + bic.system_bin as current_system_location, + bic.system_quantity as current_system_weight + FROM ScanEntries se + JOIN Users u ON se.scanned_by = u.user_id + LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number + WHERE se.session_id = ? + AND se.master_status = 'match' + AND se.duplicate_status = '00' + AND ABS(se.actual_weight - se.master_expected_weight) >= 0.01 + AND se.is_deleted = 0 + ORDER BY ABS(se.actual_weight - se.master_expected_weight) DESC + ''', [session_id]) + + elif status == 'ghost_lot': + # Ghost lots (not in master baseline) - JOIN with CURRENT + items = query_db(''' + SELECT + se.*, + u.full_name as scanned_by_name, + bic.system_bin as current_system_location, + bic.system_quantity as current_system_weight + FROM ScanEntries se + JOIN Users u ON se.scanned_by = u.user_id + LEFT JOIN BaselineInventory_Current bic ON se.lot_number = bic.lot_number + WHERE se.session_id = ? + AND se.master_status = 'ghost_lot' + AND se.is_deleted = 0 + ORDER BY se.scan_timestamp DESC + ''', [session_id]) + + elif status == 'missing': + # Missing lots (in master but not scanned) + items = query_db(''' + SELECT + bim.lot_number, + bim.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]) + else: + return jsonify({'success': False, 'message': 'Invalid status'}) + + return jsonify({ + 'success': True, + 'items': [dict(item) for item in items] if items else [] + }) + + except Exception as e: + print(f"Error in get_status_details: {str(e)}") + return jsonify({'success': False, 'message': f'Error: {str(e)}'}) \ No newline at end of file diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index 4a38ea6..e4a6585 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -6,17 +6,25 @@
- - + +
+ +

Admin Dashboard

- + + New Session
@@ -59,7 +67,7 @@
@@ -71,7 +79,7 @@
📋

No Active Sessions

Create a new count session to get started

- + Create First Session