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:
@@ -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
|
||||
39
app.py
39
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')
|
||||
|
||||
@@ -137,16 +156,6 @@ 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')
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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/<int:session_id>')
|
||||
@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))
|
||||
|
||||
@@ -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'])
|
||||
@@ -192,3 +195,43 @@ def delete_user(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)}'})
|
||||
|
||||
|
||||
@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)})
|
||||
@@ -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
|
||||
# NOTE: current_baseline_version removed - CURRENT is now global
|
||||
cursor.execute('''
|
||||
|
||||
@@ -2152,3 +2152,77 @@ 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;
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
<div class="dashboard-container">
|
||||
<!-- 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
|
||||
</button>
|
||||
<button class="mode-btn" data-href="{{ url_for('staff_mode') }}">
|
||||
@@ -38,7 +41,7 @@
|
||||
<script>
|
||||
function toggleArchived() {
|
||||
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>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<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/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 %}
|
||||
</head>
|
||||
@@ -18,7 +19,7 @@
|
||||
<nav class="navbar">
|
||||
<div class="nav-content">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<div class="count-container">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
42
templates/home.html
Normal file
42
templates/home.html
Normal 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 %}
|
||||
@@ -129,6 +129,18 @@
|
||||
</label>
|
||||
</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">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeUserModal()">Cancel</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('password').required = true;
|
||||
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';
|
||||
}
|
||||
|
||||
function openEditUser(userId) {
|
||||
editingUserId = userId;
|
||||
document.getElementById('modalTitle').textContent = 'Edit User';
|
||||
@@ -191,7 +204,22 @@ function openEditUser(userId) {
|
||||
const isEditingSelf = user.user_id === {{ session.user_id }};
|
||||
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';
|
||||
});
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
@@ -201,7 +229,6 @@ function openEditUser(userId) {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function closeUserModal() {
|
||||
document.getElementById('userModal').style.display = 'none';
|
||||
document.getElementById('userForm').reset();
|
||||
@@ -234,8 +261,23 @@ document.getElementById('userForm').addEventListener('submit', function(e) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
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();
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
closeUserModal();
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="dashboard-container">
|
||||
<div class="page-header">
|
||||
<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>
|
||||
<p class="page-subtitle">{{ count_session.session_name }}</p>
|
||||
{% if not count_session.master_baseline_timestamp %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="session-detail-container">
|
||||
<div class="session-detail-header">
|
||||
<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">
|
||||
{{ count_session.session_name }}
|
||||
{% if count_session.status == 'archived' %}<span class="archived-badge">ARCHIVED</span>{% endif %}
|
||||
@@ -694,7 +694,7 @@ function archiveSession() {
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = '{{ url_for("dashboard") }}';
|
||||
window.location.href = '{{ url_for("admin_dashboard") }}';
|
||||
} else {
|
||||
alert(data.message || 'Error archiving session');
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
<!-- Mode Selector (only for admins) -->
|
||||
{% if session.role in ['owner', 'admin'] %}
|
||||
<div class="mode-selector">
|
||||
<a href="{{ url_for('dashboard') }}" class="mode-btn">
|
||||
Admin Console
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="mode-btn">
|
||||
👔 Admin Console
|
||||
</a>
|
||||
<button class="mode-btn mode-btn-active">
|
||||
Scanning Mode
|
||||
📦 Scanning Mode
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user