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

@@ -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>

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/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>

View File

@@ -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>

View File

@@ -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
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>
</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;
document.getElementById('userModal').style.display = 'flex';
// 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) {
closeUserModal();
location.reload();
// 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);
}

View File

@@ -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 %}

View File

@@ -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');
}

View File

@@ -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>