diff --git a/app.py b/app.py index 4c7e888..c669e8e 100644 --- a/app.py +++ b/app.py @@ -36,7 +36,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1) # 1. Define the version -APP_VERSION = '0.9.0' +APP_VERSION = '0.10.0' # 2. Inject it into all templates automatically @app.context_processor @@ -94,20 +94,38 @@ def dashboard(): if role in ['owner', 'admin']: # Admin dashboard - active_sessions = query_db(''' - SELECT s.*, u.full_name as created_by_name, - COUNT(DISTINCT lc.location_count_id) as total_locations, - SUM(CASE WHEN lc.status = 'completed' THEN 1 ELSE 0 END) as completed_locations, - SUM(CASE WHEN lc.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress_locations - FROM CountSessions s - LEFT JOIN Users u ON s.created_by = u.user_id - LEFT JOIN LocationCounts lc ON s.session_id = lc.session_id - WHERE s.status = 'active' - GROUP BY s.session_id - ORDER BY s.created_timestamp DESC - ''') + show_archived = request.args.get('show_archived', '0') == '1' - return render_template('admin_dashboard.html', sessions=active_sessions) + if show_archived: + # Show all sessions (active and archived) + sessions_list = query_db(''' + SELECT s.*, u.full_name as created_by_name, + COUNT(DISTINCT lc.location_count_id) as total_locations, + SUM(CASE WHEN lc.status = 'completed' THEN 1 ELSE 0 END) as completed_locations, + SUM(CASE WHEN lc.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress_locations + FROM CountSessions s + LEFT JOIN Users u ON s.created_by = u.user_id + LEFT JOIN LocationCounts lc ON s.session_id = lc.session_id + WHERE s.status IN ('active', 'archived') + GROUP BY s.session_id + ORDER BY s.status ASC, s.created_timestamp DESC + ''') + else: + # Show only active sessions + sessions_list = query_db(''' + SELECT s.*, u.full_name as created_by_name, + COUNT(DISTINCT lc.location_count_id) as total_locations, + SUM(CASE WHEN lc.status = 'completed' THEN 1 ELSE 0 END) as completed_locations, + SUM(CASE WHEN lc.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress_locations + FROM CountSessions s + LEFT JOIN Users u ON s.created_by = u.user_id + LEFT JOIN LocationCounts lc ON s.session_id = lc.session_id + WHERE s.status = 'active' + GROUP BY s.session_id + ORDER BY s.created_timestamp DESC + ''') + + return render_template('admin_dashboard.html', sessions=sessions_list, show_archived=show_archived) else: # Staff dashboard diff --git a/blueprints/__pycache__/counting.cpython-313.pyc b/blueprints/__pycache__/counting.cpython-313.pyc index 6fa32d0..d337f5f 100644 Binary files a/blueprints/__pycache__/counting.cpython-313.pyc and b/blueprints/__pycache__/counting.cpython-313.pyc differ diff --git a/blueprints/__pycache__/sessions.cpython-313.pyc b/blueprints/__pycache__/sessions.cpython-313.pyc index d9aa405..6364114 100644 Binary files a/blueprints/__pycache__/sessions.cpython-313.pyc and b/blueprints/__pycache__/sessions.cpython-313.pyc differ diff --git a/blueprints/counting.py b/blueprints/counting.py index 58e4b1c..5872f1c 100644 --- a/blueprints/counting.py +++ b/blueprints/counting.py @@ -3,6 +3,12 @@ from db import query_db, execute_db, get_db from utils import login_required counting_bp = Blueprint('counting', __name__) +def get_active_session(session_id): + """Get session if it exists and is not archived. Returns None if invalid.""" + sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True) + if not sess or sess['status'] == 'archived': + return None + return sess @counting_bp.route('/count/') @login_required @@ -29,6 +35,10 @@ def my_counts(session_id): flash('Session not found', 'danger') return redirect(url_for('dashboard')) + if sess['status'] == 'archived': + flash('This session has been archived', 'warning') + return redirect(url_for('dashboard')) + # Get this user's active bins active_bins = query_db(''' SELECT lc.*, @@ -65,6 +75,11 @@ def my_counts(session_id): @login_required def start_bin_count(session_id): """Start counting a new bin""" + sess = get_active_session(session_id) + if not sess: + flash('Session not found or archived', 'warning') + return redirect(url_for('dashboard')) + location_name = request.form.get('location_name', '').strip().upper() if not location_name: @@ -125,7 +140,10 @@ def complete_location(location_count_id): def count_location(session_id, location_count_id): """Count lots in a specific location""" # Get session info to determine type (Cycle Count vs Physical) - sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True) + sess = get_active_session(session_id) + if not sess: + flash('Session not found or archived', 'warning') + return redirect(url_for('dashboard')) location = query_db(''' SELECT * FROM LocationCounts @@ -181,6 +199,10 @@ def count_location(session_id, location_count_id): @login_required def scan_lot(session_id, location_count_id): """Process a lot scan with duplicate detection""" + sess = get_active_session(session_id) + if not sess: + return jsonify({'success': False, 'message': 'Session not found or archived'}) + data = request.get_json() lot_number = data.get('lot_number', '').strip() weight = data.get('weight') @@ -586,6 +608,10 @@ def recalculate_duplicate_status(session_id, lot_number, current_location): @login_required def finish_location(session_id, location_count_id): """Finish counting a location""" + sess = get_active_session(session_id) + if not sess: + return jsonify({'success': False, 'message': 'Session not found or archived'}) + # Get location info location = query_db('SELECT * FROM LocationCounts WHERE location_count_id = ?', [location_count_id], one=True) diff --git a/blueprints/sessions.py b/blueprints/sessions.py index 6e6a7c6..8c70b69 100644 --- a/blueprints/sessions.py +++ b/blueprints/sessions.py @@ -208,4 +208,37 @@ def get_status_details(session_id, status): 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 + return jsonify({'success': False, 'message': f'Error: {str(e)}'}) + +@sessions_bp.route('/session//archive', methods=['POST']) +@role_required('owner', 'admin') +def archive_session(session_id): + """Archive a count session""" + sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True) + + if not sess: + return jsonify({'success': False, 'message': 'Session not found'}) + + if sess['status'] == 'archived': + return jsonify({'success': False, 'message': 'Session is already archived'}) + + execute_db('UPDATE CountSessions SET status = ? WHERE session_id = ?', ['archived', session_id]) + + return jsonify({'success': True, 'message': 'Session archived successfully'}) + + +@sessions_bp.route('/session//activate', methods=['POST']) +@role_required('owner', 'admin') +def activate_session(session_id): + """Reactivate an archived session""" + sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True) + + if not sess: + return jsonify({'success': False, 'message': 'Session not found'}) + + if sess['status'] != 'archived': + return jsonify({'success': False, 'message': 'Session is not archived'}) + + 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 diff --git a/static/css/style.css b/static/css/style.css index beb72ed..945a233 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -2118,4 +2118,37 @@ body { .scroll-to-top, .scroll-to-bottom { display: none; +} + +/* ==================== ARCHIVED SESSIONS ==================== */ + +.session-archived { + opacity: 0.7; + border-color: var(--color-text-dim); +} + +.session-archived:hover { + opacity: 0.85; +} + +.archived-badge { + display: inline-block; + padding: 0.15rem 0.5rem; + background: var(--color-text-dim); + color: var(--color-bg); + border-radius: var(--radius-sm); + font-size: 0.65rem; + font-weight: 700; + margin-left: var(--space-sm); + vertical-align: middle; +} +/* Session Detail Header Actions */ +.session-detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.session-actions-header { + flex-shrink: 0; } \ No newline at end of file diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index e4a6585..cb3fcaf 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -23,18 +23,34 @@
-

Admin Dashboard

+
+

Admin Dashboard

+ +
+ New Session
+ + {% if sessions %}
{% for session in sessions %} -
+
-

{{ session.session_name }}

+

+ {{ session.session_name }} + {% if session.status == 'archived' %}ARCHIVED{% endif %} +

{{ 'Full Physical' if session.session_type == 'full_physical' else 'Cycle Count' }} diff --git a/templates/session_detail.html b/templates/session_detail.html index 446cf82..dc3af59 100644 --- a/templates/session_detail.html +++ b/templates/session_detail.html @@ -6,13 +6,22 @@
- ← Back to Dashboard -

{{ count_session.session_name }}

- + ← Back to Dashboard +

+ {{ count_session.session_name }} + {% if count_session.status == 'archived' %}ARCHIVED{% endif %} +

{{ 'Full Physical' if count_session.session_type == 'full_physical' else 'Cycle Count' }}
+
+ {% if count_session.status == 'archived' %} + + {% else %} + + {% endif %} +
@@ -675,5 +684,38 @@ document.addEventListener('keydown', function(e) { closeReopenConfirm(); } }); +function archiveSession() { + if (!confirm('Archive this session? It will be hidden from the main dashboard but can be reactivated later.')) return; + + fetch('{{ url_for("sessions.archive_session", session_id=count_session.session_id) }}', { + method: 'POST', + headers: {'Content-Type': 'application/json'} + }) + .then(r => r.json()) + .then(data => { + if (data.success) { + window.location.href = '{{ url_for("dashboard") }}'; + } else { + alert(data.message || 'Error archiving session'); + } + }); +} + +function activateSession() { + if (!confirm('Reactivate this session? It will appear on the main dashboard again.')) return; + + fetch('{{ url_for("sessions.activate_session", session_id=count_session.session_id) }}', { + method: 'POST', + headers: {'Content-Type': 'application/json'} + }) + .then(r => r.json()) + .then(data => { + if (data.success) { + location.reload(); + } else { + alert(data.message || 'Error activating session'); + } + }); +} {% endblock %} \ No newline at end of file