226 lines
8.0 KiB
Python
226 lines
8.0 KiB
Python
"""
|
|
ScanLook - Inventory Management System
|
|
Flask Application
|
|
Production-Ready Release
|
|
"""
|
|
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, send_from_directory
|
|
from werkzeug.security import check_password_hash
|
|
import os
|
|
from datetime import timedelta
|
|
|
|
# Initialize App FIRST to prevent circular import errors
|
|
app = Flask(__name__)
|
|
|
|
# Now import your custom modules
|
|
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 blueprints.admin_locations import admin_locations_bp
|
|
from blueprints.counting import counting_bp
|
|
from blueprints.cons_sheets import cons_sheets_bp
|
|
from utils import login_required
|
|
|
|
# Register Blueprints
|
|
app.register_blueprint(data_imports_bp)
|
|
app.register_blueprint(users_bp)
|
|
app.register_blueprint(sessions_bp)
|
|
app.register_blueprint(admin_locations_bp)
|
|
app.register_blueprint(counting_bp)
|
|
app.register_blueprint(cons_sheets_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')
|
|
app.config['DATABASE'] = os.path.join(os.path.dirname(__file__), 'database', 'scanlook.db')
|
|
|
|
# Session timeout: 1 hour (auto-logout after idle period)
|
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
|
|
|
|
|
|
# 1. Define the version
|
|
APP_VERSION = '0.13.0'
|
|
|
|
# 2. Inject it into all templates automatically
|
|
@app.context_processor
|
|
def inject_version():
|
|
return dict(version=APP_VERSION)
|
|
|
|
# Auto-initialize database if it doesn't exist
|
|
db_path = os.path.join(os.path.dirname(__file__), 'database', 'scanlook.db')
|
|
if not os.path.exists(db_path):
|
|
print("Database not found, initializing...")
|
|
from database.init_db import init_database, create_default_users
|
|
init_database()
|
|
create_default_users()
|
|
print("Database initialized!")
|
|
|
|
# Run migrations to apply any pending database changes
|
|
from migrations import run_migrations
|
|
run_migrations()
|
|
|
|
|
|
# ==================== ROUTES: AUTHENTICATION ====================
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Landing page - redirect based on login status"""
|
|
if 'user_id' in session:
|
|
return redirect(url_for('home'))
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
"""Login page"""
|
|
if request.method == 'POST':
|
|
username = request.form.get('username', '').strip()
|
|
password = request.form.get('password', '')
|
|
|
|
user = query_db('SELECT * FROM Users WHERE username = ? AND is_active = 1', [username], one=True)
|
|
|
|
if user and check_password_hash(user['password'], password):
|
|
session.permanent = True # Enable session timeout
|
|
session['user_id'] = user['user_id']
|
|
session['username'] = user['username']
|
|
session['full_name'] = user['full_name']
|
|
session['role'] = user['role']
|
|
flash(f'Welcome back, {user["full_name"]}!', 'success')
|
|
return redirect(url_for('home'))
|
|
else:
|
|
flash('Invalid username or password', 'danger')
|
|
|
|
return render_template('login.html')
|
|
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
"""Logout"""
|
|
session.clear()
|
|
flash('You have been logged out', 'info')
|
|
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('/admin')
|
|
@login_required
|
|
def admin_dashboard():
|
|
"""Main dashboard - different views for admin vs staff"""
|
|
role = session.get('role')
|
|
|
|
if role in ['owner', 'admin']:
|
|
# Admin dashboard
|
|
show_archived = request.args.get('show_archived', '0') == '1'
|
|
|
|
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)
|
|
|
|
|
|
|
|
@app.route('/staff-mode')
|
|
@login_required
|
|
def staff_mode():
|
|
"""Allow admin/owner to switch to staff view for scanning"""
|
|
# Show staff dashboard view regardless of role
|
|
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, is_admin_mode=True)
|
|
|
|
|
|
|
|
# ==================== PWA SUPPORT ROUTES ====================
|
|
|
|
@app.route('/manifest.json')
|
|
def serve_manifest():
|
|
"""Serve the PWA manifest file from the static directory"""
|
|
return send_from_directory('static', 'manifest.json')
|
|
|
|
@app.route('/sw.js')
|
|
def serve_sw():
|
|
"""Serve the Service Worker file from the static directory"""
|
|
return send_from_directory('static', 'sw.js')
|
|
|
|
|
|
# ==================== Temp delete me later ====================
|
|
@app.route('/whatami')
|
|
def whatami():
|
|
"""Temporary route to identify device user-agent"""
|
|
ua = request.headers.get('User-Agent', 'Unknown')
|
|
return f"""
|
|
<html>
|
|
<head><meta name="viewport" content="width=device-width, initial-scale=1"></head>
|
|
<body style="font-family: monospace; padding: 20px; word-wrap: break-word;">
|
|
<h1>Device Info</h1>
|
|
<h3>User-Agent:</h3>
|
|
<p style="background: #eee; padding: 10px;">{ua}</p>
|
|
<h3>Screen Size:</h3>
|
|
<p id="screen" style="background: #eee; padding: 10px;">Loading...</p>
|
|
<h3>Viewport Size:</h3>
|
|
<p id="viewport" style="background: #eee; padding: 10px;">Loading...</p>
|
|
<script>
|
|
document.getElementById('screen').textContent =
|
|
'Screen: ' + screen.width + ' x ' + screen.height + ' | Pixel Ratio: ' + window.devicePixelRatio;
|
|
document.getElementById('viewport').textContent =
|
|
'Viewport: ' + window.innerWidth + ' x ' + window.innerHeight;
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
# ==================== RUN APPLICATION ====================
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True, host='0.0.0.0', port=5000) |