From 672591c73684a51af4d43d821e23cad20ea63e45 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 25 Jan 2026 02:23:18 -0600 Subject: [PATCH] v0.10.0 - Add session archive/activate feature with access controls --- app.py | 46 ++++++++++++----- .../__pycache__/counting.cpython-313.pyc | Bin 22338 -> 23369 bytes .../__pycache__/sessions.cpython-313.pyc | Bin 10041 -> 11433 bytes blueprints/counting.py | 28 +++++++++- blueprints/sessions.py | 35 ++++++++++++- static/css/style.css | 33 ++++++++++++ templates/admin_dashboard.html | 22 ++++++-- templates/session_detail.html | 48 ++++++++++++++++-- 8 files changed, 190 insertions(+), 22 deletions(-) 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 6fa32d054974ce1f458a0956c5da557c13e55ac3..d337f5f28c2860b6990b82075b2898b26d65c8f8 100644 GIT binary patch delta 6542 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 delta 5872 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 KXwx=0$iD!io7T4g diff --git a/blueprints/__pycache__/sessions.cpython-313.pyc b/blueprints/__pycache__/sessions.cpython-313.pyc index d9aa4057349d3285d87c1a5913b99a3682bde5d9..636411440350458fbe6cdcfcd349bdafbcd8b8a3 100644 GIT binary patch delta 1381 zcmb_bK}^$781C!JR%lr#+Zdwa!wnP>#$|#=QNo5W42*Rc1S6Jh>XNY~uN%k?4sTqs z7rb$uH$yz=i6h>+3}ak8a3EZT;E9v(bu9uhsEL2l@4fc_zW@C%-`@`-g@@Mt+S(cl zNB&bbp|mes8^FhRm%Tu-0aukxw0Obts?&L(< zchWvx;GxEWXsNOwTp=u%4i0nA!rG0ee9UeV}7CA$gCutAW#^+pp zm1dy?qv>oa1Le3ZC(>gq%pih&4}t20t2E5qCHjso1yeC~o##}Gp~8;#=HPqWG7LcxSMa(FcX z9z%gf=gGbkgG{apA!^1;SD8wYjEn+~^ld zsY5USVk%HSxw6YJ5)VWwojELk{5V@SYqjIKCWED)m5;}c8IA&&jRr6rbJu3Hc4%)9 zQ+5dLG<*N-Za4ORr05zg2_cmY>Glq)51y#Q(k3&b+Z!s{$4XpSWx|@hBW?A{R?Do< zOM2}iK!y#GmI{={sLUjLYc?IrCgZ&*j8o(nmpg#+qV;`#m$^%WAlUUY6kYoT)D=OU zYQMf-zs{<72Zp`^>(cy+r3By}&4V!5H&Jvm0jy!jhux+l!tV9f^#)BCJT4S}35UTR J7K%V??') @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