diff --git a/AI Prompt.txt b/AI Prompt.txt index e58f42d..96de210 100644 --- a/AI Prompt.txt +++ b/AI Prompt.txt @@ -29,6 +29,9 @@ Long-term goal: evolve into a WMS, but right now focus on making this workflow r 5) **Keep it to the point.** Default to short answers. Only explain more if I ask. 6) **Verify safety.** Warn me before destructive actions (delete/overwrite/migrations). Offer a safer alternative. 7) **Evidence-based debugging.** Ask for exact error text/logs and versions before guessing. +8) **CSS changes:** Ask which device(s) the change is for (desktop/mobile/scanner) before editing. Each has its own file. +9) **Database changes:** The app auto-initializes the database if it doesn't exist. Schema is in /database/init_db.py. +10) **Docker deployment:** Production runs in Docker on Linux (jisoo). Volume mounts only /app/database to preserve data between updates. ## How you should respond - Start by confirming which mode we’re working on: Cycle Count or Physical Inventory. @@ -37,10 +40,53 @@ Long-term goal: evolve into a WMS, but right now focus on making this workflow r - When writing SQL: be explicit about constraints/indexes that matter for lots/bins/sessions. - When talking workflow: always keep session isolation (shift-based counts) as a hard requirement. -## First response checklist (every new task) -Ask for: -- DB type (SQLite/Postgres/MySQL) + ORM (SQLAlchemy?) or raw SQL -- Current data model (tables or SQLAlchemy models) for: count_session, bin/location, expected_lines, scans -- How the Master Inventory list is formatted (CSV columns) -- What β€œFinalize BIN” should do exactly (lock? allow reopen? who can override?) -Then proceed one step at a time. +## Scanlook (current product summary) +Scanlook is a web app for warehouse counting workflows built with Flask + SQLite. + +**Current Version:** 0.11.3 + +**Tech Stack:** +- Backend: Python/Flask, raw SQL (no ORM) +- Database: SQLite (located in /database/scanlook.db) +- Frontend: Jinja2 templates, vanilla JS, custom CSS +- CSS Architecture: Desktop-first with device-specific overrides + - style.css (base/desktop) + - mobile.css (phones, 360-767px) + - scanner.css (MC9300 scanners, max-width 359px) +- Deployment: Docker container, Gitea for version control + container registry + +**Project Structure:** +- app.py (main Flask app, routes for auth + dashboard) +- /blueprints/ (modular routes: counting.py, sessions.py, users.py, data_imports.py, admin_locations.py) +- /templates/ (Jinja2 HTML templates) +- /static/css/ (style.css, mobile.css, scanner.css) +- /database/ (scanlook.db, init_db.py) +- db.py (database helper functions: query_db, execute_db) +- utils.py (decorators: login_required, role_required) + +**Key Features (implemented):** +- Count Sessions with archive/activate functionality +- Master baseline upload (CSV) +- Current baseline upload (optional, for comparison) +- Staff scanning interface optimized for MC9300 Zebra scanners +- Scan statuses: Match, Duplicate, Wrong Location, Ghost Lot, Weight Discrepancy +- Location/BIN workflow with Expected β†’ Scanned flow +- Session isolation (archived sessions blocked from access) +- Role-based access: owner, admin, staff +- Auto-initialize database on first run + +**Two count types:** +1. Cycle Count: shows Expected list for the BIN +2. Physical Inventory: blind count (no Expected list shown) + +**Long-term goal:** Modular WMS with future modules for Shipping, Receiving, Transfers, Production. + + + +## Quick Reference +- Database: SQLite at /database/scanlook.db +- Scanner viewport: 320px wide (MC9300) +- Mobile breakpoint: 360-767px +- Desktop: 768px+ +- Git remote: http://10.44.44.33:3000/stuff/ScanLook.git +- Docker registry: 10.44.44.33:3000/stuff/scanlook \ No newline at end of file diff --git a/app.py b/app.py index 26adfe7..c1a4839 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.11.3' +APP_VERSION = '0.12.0' # 2. Inject it into all templates automatically @app.context_processor @@ -59,7 +59,7 @@ if not os.path.exists(db_path): def index(): """Landing page - redirect based on login status""" if 'user_id' in session: - return redirect(url_for('dashboard')) + return redirect(url_for('home')) return redirect(url_for('login')) @@ -79,7 +79,7 @@ def login(): session['full_name'] = user['full_name'] session['role'] = user['role'] flash(f'Welcome back, {user["full_name"]}!', 'success') - return redirect(url_for('dashboard')) + return redirect(url_for('home')) else: flash('Invalid username or password', 'danger') @@ -94,11 +94,30 @@ def logout(): return redirect(url_for('login')) +# ==================== ROUTES: HOME ==================== +@app.route('/home') +@login_required +def home(): + """Module selection landing page""" + user_id = session.get('user_id') + + # Get modules this user has access to + modules = query_db(''' + SELECT m.module_id, m.module_name, m.module_key, m.description, m.icon + FROM Modules m + JOIN UserModules um ON m.module_id = um.module_id + WHERE um.user_id = ? AND m.is_active = 1 + ORDER BY m.display_order + ''', [user_id]) + + return render_template('home.html', modules=modules) + + # ==================== ROUTES: DASHBOARD ==================== -@app.route('/dashboard') +@app.route('/admin') @login_required -def dashboard(): +def admin_dashboard(): """Main dashboard - different views for admin vs staff""" role = session.get('role') @@ -136,17 +155,7 @@ def dashboard(): ''') return render_template('admin_dashboard.html', sessions=sessions_list, show_archived=show_archived) - - else: - # Staff dashboard - active_sessions = query_db(''' - SELECT session_id, session_name, session_type, created_timestamp - FROM CountSessions - WHERE status = 'active' - ORDER BY created_timestamp DESC - ''') - - return render_template('staff_dashboard.html', sessions=active_sessions) + @app.route('/staff-mode') diff --git a/blueprints/__pycache__/counting.cpython-313.pyc b/blueprints/__pycache__/counting.cpython-313.pyc index 4f56345..a22ef0d 100644 Binary files a/blueprints/__pycache__/counting.cpython-313.pyc and b/blueprints/__pycache__/counting.cpython-313.pyc differ diff --git a/blueprints/__pycache__/users.cpython-313.pyc b/blueprints/__pycache__/users.cpython-313.pyc index 0de51c9..1718ceb 100644 Binary files a/blueprints/__pycache__/users.cpython-313.pyc and b/blueprints/__pycache__/users.cpython-313.pyc differ diff --git a/blueprints/counting.py b/blueprints/counting.py index 6f4f078..0c258b5 100644 --- a/blueprints/counting.py +++ b/blueprints/counting.py @@ -10,6 +10,32 @@ def get_active_session(session_id): return None return sess +@counting_bp.route('/counts') +@login_required +def index(): + """Counts module landing - show active sessions""" + # Check if user has access to this module + user_id = session.get('user_id') + has_access = query_db(''' + SELECT 1 FROM UserModules um + JOIN Modules m ON um.module_id = m.module_id + WHERE um.user_id = ? AND m.module_key = 'counting' AND m.is_active = 1 + ''', [user_id], one=True) + + if not has_access: + flash('You do not have access to this module', 'danger') + return redirect(url_for('home')) + + active_sessions = query_db(''' + SELECT session_id, session_name, session_type, created_timestamp + FROM CountSessions + WHERE status = 'active' + ORDER BY created_timestamp DESC + ''') + + return render_template('staff_dashboard.html', sessions=active_sessions) + + @counting_bp.route('/count/') @login_required def count_session(session_id): @@ -19,7 +45,7 @@ def count_session(session_id): if not sess: flash('Session not found or not active', 'danger') - return redirect(url_for('dashboard')) + return redirect(url_for('counting.index')) # Redirect to my_counts page (staff can manage multiple bins) return redirect(url_for('counting.my_counts', session_id=session_id)) @@ -33,11 +59,11 @@ def my_counts(session_id): if not sess: flash('Session not found', 'danger') - return redirect(url_for('dashboard')) + return redirect(url_for('counting.index')) if sess['status'] == 'archived': flash('This session has been archived', 'warning') - return redirect(url_for('dashboard')) + return redirect(url_for('counting.index')) # Get this user's active bins active_bins = query_db(''' @@ -78,7 +104,7 @@ def start_bin_count(session_id): sess = get_active_session(session_id) if not sess: flash('Session not found or archived', 'warning') - return redirect(url_for('dashboard')) + return redirect(url_for('counting.index')) if not sess['master_baseline_timestamp']: flash('Master File not uploaded. Please upload it before starting bins.', 'warning') return redirect(url_for('counting.my_counts', session_id=session_id)) @@ -146,7 +172,7 @@ def count_location(session_id, location_count_id): sess = get_active_session(session_id) if not sess: flash('Session not found or archived', 'warning') - return redirect(url_for('dashboard')) + return redirect(url_for('counting.index')) if not sess['master_baseline_timestamp']: flash('Master File not uploaded. Please upload it before starting bins.', 'warning') return redirect(url_for('counting.my_counts', session_id=session_id)) diff --git a/blueprints/users.py b/blueprints/users.py index 47c1acd..6176365 100644 --- a/blueprints/users.py +++ b/blueprints/users.py @@ -17,7 +17,10 @@ def manage_users(): # Admins can only see staff users = query_db("SELECT * FROM Users WHERE role = 'staff' ORDER BY full_name") - return render_template('manage_users.html', users=users) + # Get all active modules + modules = query_db('SELECT * FROM Modules WHERE is_active = 1 ORDER BY display_order') + + return render_template('manage_users.html', users=users, modules=modules) @users_bp.route('/settings/users/add', methods=['POST']) @@ -191,4 +194,44 @@ def delete_user(user_id): execute_db('UPDATE Users SET is_active = 0 WHERE user_id = ?', [user_id]) return jsonify({'success': True, 'message': 'User deleted successfully'}) except Exception as e: - return jsonify({'success': False, 'message': f'Error deleting user: {str(e)}'}) \ No newline at end of file + return jsonify({'success': False, 'message': f'Error deleting user: {str(e)}'}) + + +@users_bp.route('/settings/users//modules', methods=['GET']) +@role_required('owner', 'admin') +def get_user_modules(user_id): + """Get modules assigned to a user""" + modules = query_db(''' + SELECT module_id FROM UserModules WHERE user_id = ? + ''', [user_id]) + + module_ids = [m['module_id'] for m in modules] + return jsonify({'success': True, 'module_ids': module_ids}) + + +@users_bp.route('/settings/users//modules', methods=['POST']) +@role_required('owner', 'admin') +def update_user_modules(user_id): + """Update modules assigned to a user""" + data = request.get_json() + module_ids = data.get('module_ids', []) + + # Verify user exists + user = query_db('SELECT user_id FROM Users WHERE user_id = ?', [user_id], one=True) + if not user: + return jsonify({'success': False, 'message': 'User not found'}) + + try: + # Remove all current assignments + execute_db('DELETE FROM UserModules WHERE user_id = ?', [user_id]) + + # Add new assignments + for module_id in module_ids: + execute_db(''' + INSERT INTO UserModules (user_id, module_id, granted_by) + VALUES (?, ?, ?) + ''', [user_id, module_id, session['user_id']]) + + return jsonify({'success': True, 'message': 'Modules updated'}) + except Exception as e: + return jsonify({'success': False, 'message': str(e)}) \ No newline at end of file diff --git a/database/init_db.py b/database/init_db.py index 5784954..8409b28 100644 --- a/database/init_db.py +++ b/database/init_db.py @@ -31,7 +31,36 @@ def init_database(): created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') - + + # Modules Table - defines available system modules + cursor.execute(''' + CREATE TABLE IF NOT EXISTS Modules ( + module_id INTEGER PRIMARY KEY AUTOINCREMENT, + module_name TEXT UNIQUE NOT NULL, + module_key TEXT UNIQUE NOT NULL, + description TEXT, + icon TEXT, + is_active INTEGER DEFAULT 1, + display_order INTEGER DEFAULT 0 + ) + ''') + + # UserModules Table - which modules each user can access + cursor.execute(''' + CREATE TABLE IF NOT EXISTS UserModules ( + user_module_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + module_id INTEGER NOT NULL, + granted_by INTEGER, + granted_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES Users(user_id), + FOREIGN KEY (module_id) REFERENCES Modules(module_id), + FOREIGN KEY (granted_by) REFERENCES Users(user_id), + UNIQUE(user_id, module_id) + ) + ''') + + # CountSessions Table # NOTE: current_baseline_version removed - CURRENT is now global cursor.execute(''' diff --git a/static/css/style.css b/static/css/style.css index 945a233..a7319d6 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -2151,4 +2151,78 @@ body { .session-actions-header { flex-shrink: 0; +} + +/* ==================== MODULE GRID (Home Page) ==================== */ + +.module-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--space-xl); + margin-top: var(--space-xl); +} + +.module-card { + background: var(--color-surface); + border: 2px solid var(--color-border); + border-radius: var(--radius-xl); + padding: var(--space-2xl) var(--space-xl); + text-decoration: none; + text-align: center; + transition: var(--transition); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-md); +} + +.module-card:hover { + border-color: var(--color-primary); + transform: translateY(-4px); + box-shadow: var(--shadow-glow), var(--shadow-lg); +} + +.module-icon { + width: 80px; + height: 80px; + background: var(--color-primary-glow); + border: 2px solid var(--color-primary); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + color: var(--color-primary); + transition: var(--transition); +} + +.module-card:hover .module-icon { + background: var(--color-primary); + color: var(--color-bg); + box-shadow: 0 0 30px var(--color-primary-glow); +} + +.module-name { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-text); + margin: 0; +} + +.module-desc { + font-size: 0.9rem; + color: var(--color-text-muted); + margin: 0; + line-height: 1.5; +} +/* ==================== MODAL SCROLL FIX ==================== */ + +.modal { + overflow-y: auto; + padding: var(--space-xl) 0; +} + +.modal-content { + max-height: none; + margin: auto; } \ No newline at end of file diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index cb3fcaf..9768058 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -6,7 +6,10 @@
-
diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..37396c5 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %}Home - ScanLook{% endblock %} + +{% block content %} +
+ + + {% if session.role in ['owner', 'admin'] %} + + {% endif %} + +
+

Welcome, {{ session.full_name }}

+

Select a module to get started

+
+ + {% if modules %} +
+ {% for m in modules %} + +
+ +
+

{{ m.module_name }}

+

{{ m.description }}

+
+ {% endfor %} +
+ {% else %} +
+
πŸ”’
+

No Modules Available

+

You don't have access to any modules. Please contact your administrator.

+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/manage_users.html b/templates/manage_users.html index e288e57..25f2ff7 100644 --- a/templates/manage_users.html +++ b/templates/manage_users.html @@ -129,6 +129,18 @@
+
+ +
+ {% for module in modules %} + + {% endfor %} +
+
+