v0.12.0 - Add modular system architecture with user-based module access

- Add Modules and UserModules database tables
- Create home page with module selection grid
- Implement per-user module assignment in user management
- Add route guards for module access control
- Refactor navigation: login -> home -> modules, admin console via button
- Add Font Awesome icons
This commit is contained in:
Javier
2026-01-26 11:35:29 -06:00
parent cbd7e535e6
commit 21671d6bee
17 changed files with 365 additions and 47 deletions

View File

@@ -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. 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. 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. 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 ## How you should respond
- Start by confirming which mode were working on: Cycle Count or Physical Inventory. - Start by confirming which mode were 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 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. - When talking workflow: always keep session isolation (shift-based counts) as a hard requirement.
## First response checklist (every new task) ## Scanlook (current product summary)
Ask for: Scanlook is a web app for warehouse counting workflows built with Flask + SQLite.
- 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 **Current Version:** 0.11.3
- How the Master Inventory list is formatted (CSV columns)
- What “Finalize BIN” should do exactly (lock? allow reopen? who can override?) **Tech Stack:**
Then proceed one step at a time. - 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

39
app.py
View File

@@ -36,7 +36,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
# 1. Define the version # 1. Define the version
APP_VERSION = '0.11.3' APP_VERSION = '0.12.0'
# 2. Inject it into all templates automatically # 2. Inject it into all templates automatically
@app.context_processor @app.context_processor
@@ -59,7 +59,7 @@ if not os.path.exists(db_path):
def index(): def index():
"""Landing page - redirect based on login status""" """Landing page - redirect based on login status"""
if 'user_id' in session: if 'user_id' in session:
return redirect(url_for('dashboard')) return redirect(url_for('home'))
return redirect(url_for('login')) return redirect(url_for('login'))
@@ -79,7 +79,7 @@ def login():
session['full_name'] = user['full_name'] session['full_name'] = user['full_name']
session['role'] = user['role'] session['role'] = user['role']
flash(f'Welcome back, {user["full_name"]}!', 'success') flash(f'Welcome back, {user["full_name"]}!', 'success')
return redirect(url_for('dashboard')) return redirect(url_for('home'))
else: else:
flash('Invalid username or password', 'danger') flash('Invalid username or password', 'danger')
@@ -94,11 +94,30 @@ def logout():
return redirect(url_for('login')) 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 ==================== # ==================== ROUTES: DASHBOARD ====================
@app.route('/dashboard') @app.route('/admin')
@login_required @login_required
def dashboard(): def admin_dashboard():
"""Main dashboard - different views for admin vs staff""" """Main dashboard - different views for admin vs staff"""
role = session.get('role') role = session.get('role')
@@ -137,16 +156,6 @@ def dashboard():
return render_template('admin_dashboard.html', sessions=sessions_list, show_archived=show_archived) 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') @app.route('/staff-mode')

View File

@@ -10,6 +10,32 @@ def get_active_session(session_id):
return None return None
return sess 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/<int:session_id>') @counting_bp.route('/count/<int:session_id>')
@login_required @login_required
def count_session(session_id): def count_session(session_id):
@@ -19,7 +45,7 @@ def count_session(session_id):
if not sess: if not sess:
flash('Session not found or not active', 'danger') 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) # Redirect to my_counts page (staff can manage multiple bins)
return redirect(url_for('counting.my_counts', session_id=session_id)) return redirect(url_for('counting.my_counts', session_id=session_id))
@@ -33,11 +59,11 @@ def my_counts(session_id):
if not sess: if not sess:
flash('Session not found', 'danger') flash('Session not found', 'danger')
return redirect(url_for('dashboard')) return redirect(url_for('counting.index'))
if sess['status'] == 'archived': if sess['status'] == 'archived':
flash('This session has been archived', 'warning') flash('This session has been archived', 'warning')
return redirect(url_for('dashboard')) return redirect(url_for('counting.index'))
# Get this user's active bins # Get this user's active bins
active_bins = query_db(''' active_bins = query_db('''
@@ -78,7 +104,7 @@ def start_bin_count(session_id):
sess = get_active_session(session_id) sess = get_active_session(session_id)
if not sess: if not sess:
flash('Session not found or archived', 'warning') flash('Session not found or archived', 'warning')
return redirect(url_for('dashboard')) return redirect(url_for('counting.index'))
if not sess['master_baseline_timestamp']: if not sess['master_baseline_timestamp']:
flash('Master File not uploaded. Please upload it before starting bins.', 'warning') flash('Master File not uploaded. Please upload it before starting bins.', 'warning')
return redirect(url_for('counting.my_counts', session_id=session_id)) 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) sess = get_active_session(session_id)
if not sess: if not sess:
flash('Session not found or archived', 'warning') flash('Session not found or archived', 'warning')
return redirect(url_for('dashboard')) return redirect(url_for('counting.index'))
if not sess['master_baseline_timestamp']: if not sess['master_baseline_timestamp']:
flash('Master File not uploaded. Please upload it before starting bins.', 'warning') flash('Master File not uploaded. Please upload it before starting bins.', 'warning')
return redirect(url_for('counting.my_counts', session_id=session_id)) return redirect(url_for('counting.my_counts', session_id=session_id))

View File

@@ -17,7 +17,10 @@ def manage_users():
# Admins can only see staff # Admins can only see staff
users = query_db("SELECT * FROM Users WHERE role = 'staff' ORDER BY full_name") 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']) @users_bp.route('/settings/users/add', methods=['POST'])
@@ -192,3 +195,43 @@ def delete_user(user_id):
return jsonify({'success': True, 'message': 'User deleted successfully'}) return jsonify({'success': True, 'message': 'User deleted successfully'})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'Error deleting user: {str(e)}'}) return jsonify({'success': False, 'message': f'Error deleting user: {str(e)}'})
@users_bp.route('/settings/users/<int:user_id>/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/<int:user_id>/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)})

View File

@@ -32,6 +32,35 @@ def init_database():
) )
''') ''')
# 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 # CountSessions Table
# NOTE: current_baseline_version removed - CURRENT is now global # NOTE: current_baseline_version removed - CURRENT is now global
cursor.execute(''' cursor.execute('''

View File

@@ -2152,3 +2152,77 @@ body {
.session-actions-header { .session-actions-header {
flex-shrink: 0; 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;
}

View File

@@ -6,7 +6,10 @@
<div class="dashboard-container"> <div class="dashboard-container">
<!-- Mode Selector --> <!-- Mode Selector -->
<div class="mode-selector"> <div class="mode-selector">
<button class="mode-btn mode-btn-active" data-href="{{ url_for('dashboard') }}"> <a href="{{ url_for('home') }}" class="btn btn-secondary btn-sm">
<i class="fa-solid fa-arrow-left"></i> Back to Home
</a>
<button class="mode-btn mode-btn-active" data-href="{{ url_for('admin_dashboard') }}">
👔 Admin Console 👔 Admin Console
</button> </button>
<button class="mode-btn" data-href="{{ url_for('staff_mode') }}"> <button class="mode-btn" data-href="{{ url_for('staff_mode') }}">
@@ -38,7 +41,7 @@
<script> <script>
function toggleArchived() { function toggleArchived() {
const checked = document.getElementById('showArchived').checked; const checked = document.getElementById('showArchived').checked;
window.location.href = '{{ url_for("dashboard") }}' + (checked ? '?show_archived=1' : ''); window.location.href = '{{ url_for("admin_dashboard") }}' + (checked ? '?show_archived=1' : '');
} }
</script> </script>

View File

@@ -10,6 +10,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/mobile.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/mobile.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/scanner.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/scanner.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
@@ -18,7 +19,7 @@
<nav class="navbar"> <nav class="navbar">
<div class="nav-content"> <div class="nav-content">
<div class="nav-left"> <div class="nav-left">
<a href="{{ url_for('dashboard') }}" class="logo"> <a href="{{ url_for('home') }}" class="logo">
<span class="logo-scan">SCAN</span><span class="logo-look">LOOK</span> <span class="logo-scan">SCAN</span><span class="logo-look">LOOK</span>
</a> </a>
</div> </div>

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="count-container"> <div class="count-container">
<div class="count-header"> <div class="count-header">
<a href="{{ url_for('dashboard') }}" class="breadcrumb">← Back</a> <a href="{{ url_for('admin_dashboard') }}" class="breadcrumb">← Back</a>
<h1 class="page-title">{{ session.session_name }}</h1> <h1 class="page-title">{{ session.session_name }}</h1>
</div> </div>

View File

@@ -34,7 +34,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for('admin_dashboard') }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Create Session</button> <button type="submit" class="btn btn-primary">Create Session</button>
</div> </div>
</form> </form>

42
templates/home.html Normal file
View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Home - ScanLook{% endblock %}
{% block content %}
<div class="dashboard-container">
<!-- Admin Button (only for admins/owners) -->
{% if session.role in ['owner', 'admin'] %}
<div class="mode-selector">
<a href="{{ url_for('admin_dashboard') }}" class="mode-btn">
👔 Admin Console
</a>
</div>
{% endif %}
<div class="dashboard-header">
<h1 class="page-title">Welcome, {{ session.full_name }}</h1>
<p class="page-subtitle">Select a module to get started</p>
</div>
{% if modules %}
<div class="module-grid">
{% for m in modules %}
<a href="{{ url_for(m.module_key + '.index') }}" class="module-card">
<div class="module-icon">
<i class="fa-solid {{ m.icon }}"></i>
</div>
<h3 class="module-name">{{ m.module_name }}</h3>
<p class="module-desc">{{ m.description }}</p>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">🔒</div>
<h2 class="empty-title">No Modules Available</h2>
<p class="empty-text">You don't have access to any modules. Please contact your administrator.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -129,6 +129,18 @@
</label> </label>
</div> </div>
<div class="form-group">
<label class="form-label">Module Access</label>
<div class="module-checkboxes" id="moduleCheckboxes">
{% for module in modules %}
<label class="checkbox-label">
<input type="checkbox" name="modules" value="{{ module.module_id }}" class="module-checkbox">
{{ module.module_name }}
</label>
{% endfor %}
</div>
</div>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeUserModal()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="closeUserModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save User</button> <button type="submit" class="btn btn-primary">Save User</button>
@@ -162,9 +174,10 @@ function openAddUser() {
document.getElementById('passwordOptional').style.display = 'none'; document.getElementById('passwordOptional').style.display = 'none';
document.getElementById('password').required = true; document.getElementById('password').required = true;
document.getElementById('activeToggleGroup').style.display = 'none'; document.getElementById('activeToggleGroup').style.display = 'none';
// Uncheck all modules for new user
document.querySelectorAll('.module-checkbox').forEach(cb => cb.checked = false);
document.getElementById('userModal').style.display = 'flex'; document.getElementById('userModal').style.display = 'flex';
} }
function openEditUser(userId) { function openEditUser(userId) {
editingUserId = userId; editingUserId = userId;
document.getElementById('modalTitle').textContent = 'Edit User'; document.getElementById('modalTitle').textContent = 'Edit User';
@@ -191,7 +204,22 @@ function openEditUser(userId) {
const isEditingSelf = user.user_id === {{ session.user_id }}; const isEditingSelf = user.user_id === {{ session.user_id }};
document.getElementById('role').disabled = isEditingSelf; document.getElementById('role').disabled = isEditingSelf;
// Load user's modules
fetch('/settings/users/' + userId + '/modules')
.then(resp => resp.json())
.then(moduleData => {
// Uncheck all first
document.querySelectorAll('.module-checkbox').forEach(cb => cb.checked = false);
// Check assigned modules
if (moduleData.success) {
moduleData.module_ids.forEach(id => {
const cb = document.querySelector(`.module-checkbox[value="${id}"]`);
if (cb) cb.checked = true;
});
}
// Show modal after modules are loaded
document.getElementById('userModal').style.display = 'flex'; document.getElementById('userModal').style.display = 'flex';
});
} else { } else {
alert(data.message); alert(data.message);
} }
@@ -201,7 +229,6 @@ function openEditUser(userId) {
console.error(error); console.error(error);
}); });
} }
function closeUserModal() { function closeUserModal() {
document.getElementById('userModal').style.display = 'none'; document.getElementById('userModal').style.display = 'none';
document.getElementById('userForm').reset(); document.getElementById('userForm').reset();
@@ -234,8 +261,23 @@ document.getElementById('userForm').addEventListener('submit', function(e) {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
// Save modules if editing existing user
if (userId) {
const moduleIds = Array.from(document.querySelectorAll('.module-checkbox:checked'))
.map(cb => parseInt(cb.value));
fetch(`/settings/users/${userId}/modules`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ module_ids: moduleIds })
}).then(() => {
closeUserModal(); closeUserModal();
location.reload(); location.reload();
});
} else {
closeUserModal();
location.reload();
}
} else { } else {
alert(data.message); alert(data.message);
} }

View File

@@ -6,7 +6,7 @@
<div class="dashboard-container"> <div class="dashboard-container">
<div class="page-header"> <div class="page-header">
<div> <div>
<a href="{{ url_for('dashboard') }}" class="breadcrumb">← Back to Dashboard</a> <a href="{{ url_for('counting.index') }}" class="breadcrumb">← Back to Sessions</a>
<h1 class="page-title">My Active Counts</h1> <h1 class="page-title">My Active Counts</h1>
<p class="page-subtitle">{{ count_session.session_name }}</p> <p class="page-subtitle">{{ count_session.session_name }}</p>
{% if not count_session.master_baseline_timestamp %} {% if not count_session.master_baseline_timestamp %}

View File

@@ -6,7 +6,7 @@
<div class="session-detail-container"> <div class="session-detail-container">
<div class="session-detail-header"> <div class="session-detail-header">
<div> <div>
<a href="{{ url_for('dashboard') }}{% if count_session.status == 'archived' %}?show_archived=1{% endif %}" class="breadcrumb">← Back to Dashboard</a> <a href="{{ url_for('admin_dashboard') }}{% if count_session.status == 'archived' %}?show_archived=1{% endif %}" class="breadcrumb">← Back to Dashboard</a>
<h1 class="page-title"> <h1 class="page-title">
{{ count_session.session_name }} {{ count_session.session_name }}
{% if count_session.status == 'archived' %}<span class="archived-badge">ARCHIVED</span>{% endif %} {% if count_session.status == 'archived' %}<span class="archived-badge">ARCHIVED</span>{% endif %}
@@ -694,7 +694,7 @@ function archiveSession() {
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
window.location.href = '{{ url_for("dashboard") }}'; window.location.href = '{{ url_for("admin_dashboard") }}';
} else { } else {
alert(data.message || 'Error archiving session'); alert(data.message || 'Error archiving session');
} }

View File

@@ -7,16 +7,19 @@
<!-- Mode Selector (only for admins) --> <!-- Mode Selector (only for admins) -->
{% if session.role in ['owner', 'admin'] %} {% if session.role in ['owner', 'admin'] %}
<div class="mode-selector"> <div class="mode-selector">
<a href="{{ url_for('dashboard') }}" class="mode-btn"> <a href="{{ url_for('admin_dashboard') }}" class="mode-btn">
Admin Console 👔 Admin Console
</a> </a>
<button class="mode-btn mode-btn-active"> <button class="mode-btn mode-btn-active">
Scanning Mode 📦 Scanning Mode
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="dashboard-header"> <div class="dashboard-header">
<a href="{{ url_for('home') }}" class="btn btn-secondary btn-sm" style="margin-bottom: var(--space-md);">
<i class="fa-solid fa-arrow-left"></i> Back to Home
</a>
<h1 class="page-title">Select Count Session</h1> <h1 class="page-title">Select Count Session</h1>
</div> </div>