Update dashboard

This commit is contained in:
2025-09-01 10:41:22 -04:00
parent eb62501bbb
commit f3dbaf2b81
5 changed files with 1679 additions and 1402 deletions
+7
View File
@@ -31,6 +31,13 @@ app.config['TEMPLATES_AUTO_RELOAD'] = os.environ.get('TEMPLATES_AUTO_RELOAD')
# Initialize database # Initialize database
db = SQLAlchemy(app) db = SQLAlchemy(app)
@app.context_processor
def inject_company_name():
"""Make COMPANY_NAME available to all templates"""
return {
'COMPANY_NAME': os.environ.get('COMPANY_NAME', 'QR Code Management System')
}
def create_performance_indexes(): def create_performance_indexes():
"""Create performance optimization indexes""" """Create performance optimization indexes"""
try: try:
+156 -19
View File
@@ -4,31 +4,163 @@
*/ */
/* Dashboard Header */ /* Dashboard Header */
.dashboard-header { .header-stats {
display: flex; display: flex;
justify-content: space-between; align-items: center;
align-items: flex-start; }
margin-bottom: var(--spacing-8);
padding: var(--spacing-6); .header-stats-grid {
background: var(--white); display: grid;
border-radius: var(--radius-xl); grid-template-columns: repeat(2, 1fr);
box-shadow: var(--shadow); gap: 1rem;
min-width: 400px;
}
@media (min-width: 1200px) {
.header-stats-grid {
grid-template-columns: repeat(4, 1fr);
min-width: 500px;
}
}
.header-stat-card {
background: rgba(255, 255, 255, 0.95);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 0.75rem;
transition: all 0.2s ease-in-out;
backdrop-filter: blur(10px);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.dashboard-header::before { .header-stat-card::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 4px; height: 2px;
background: linear-gradient( }
90deg,
var(--primary-color), .header-stat-card.primary::before {
var(--primary-hover) background: linear-gradient(90deg, #2563eb, #1d4ed8);
); }
.header-stat-card.success::before {
background: linear-gradient(90deg, #10b981, #047857);
}
.header-stat-card.danger::before {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.header-stat-card.info::before {
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
}
.header-stat-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.15);
}
.header-stat-icon {
width: 36px;
height: 36px;
border-radius: 0.375rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: #ffffff;
flex-shrink: 0;
}
.header-stat-card.primary .header-stat-icon {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
}
.header-stat-card.success .header-stat-icon {
background: linear-gradient(135deg, #10b981, #047857);
}
.header-stat-card.danger .header-stat-icon {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.header-stat-card.info .header-stat-icon {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
}
.header-stat-content h3 {
font-size: 1.25rem;
font-weight: 700;
color: #0f172a;
margin-bottom: 0.125rem;
line-height: 1;
}
.header-stat-content p {
color: #64748b;
font-size: 0.75rem;
margin: 0;
line-height: 1.2;
}
/* Update existing dashboard header to accommodate new layout */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem;
background: linear-gradient(135deg, #a0aff1 0%, #ebdef8 100%);
border-radius: 0.75rem;
color: white;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
flex-wrap: wrap;
gap: 2rem;
}
/* Responsive Design for Header */
@media (max-width: 1024px) {
.dashboard-header {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.header-stats-grid {
grid-template-columns: repeat(2, 1fr);
min-width: 300px;
}
}
@media (max-width: 768px) {
.dashboard-header {
padding: 1.5rem;
}
.header-stats-grid {
grid-template-columns: 1fr;
min-width: 250px;
}
.header-stat-card {
padding: 0.75rem;
}
.header-stat-content h3 {
font-size: 1.125rem;
}
.header-stat-content p {
font-size: 0.6875rem;
}
} }
.welcome-section h1 { .welcome-section h1 {
@@ -892,7 +1024,7 @@
} }
.qr-url { .qr-url {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
background: var(--gray-100); background: var(--gray-100);
padding: var(--spacing-1) var(--spacing-2); padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@@ -927,7 +1059,8 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.qr-status, .qr-project { .qr-status,
.qr-project {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-1); gap: var(--spacing-1);
@@ -1088,8 +1221,12 @@
/* Animations */ /* Animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideIn { @keyframes slideIn {
+6 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}QR Code Management System{% endblock %}</title> <title>{% block title %}{{ COMPANY_NAME }}{% endblock %}</title>
<!-- Main CSS --> <!-- Main CSS -->
<link <link
@@ -87,7 +87,11 @@
</script> </script>
{% if turnstile_enabled %} {% if turnstile_enabled %}
<!-- Cloudflare Turnstile --> <!-- Cloudflare Turnstile -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
{% endif %} {% endif %}
</body> </body>
</html> </html>
+17 -10
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}QR Code Management System{% endblock %}</title> <title>{% block title %}{{ COMPANY_NAME }}{% endblock %}</title>
<!-- Main CSS --> <!-- Main CSS -->
<link <link
@@ -55,8 +55,10 @@
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('projects') }}" <a
class="menu-item {% if request.endpoint in ['projects', 'create_project', 'edit_project'] %}active{% endif %}"> href="{{ url_for('projects') }}"
class="menu-item {% if request.endpoint in ['projects', 'create_project', 'edit_project'] %}active{% endif %}"
>
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
<span class="menu-text">Projects</span> <span class="menu-text">Projects</span>
</a> </a>
@@ -76,12 +78,17 @@
<i class="fas fa-calculator"></i> <i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span> <span class="menu-text">Payroll</span>
</a> </a>
<a href="{{ url_for('qr_statistics') }}" class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}"> <a
href="{{ url_for('qr_statistics') }}"
class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}"
>
<i class="fas fa-chart-pie"></i> <i class="fas fa-chart-pie"></i>
<span class="menu-text">Statistics</span> <span class="menu-text">Statistics</span>
</a> </a>
<a href="{{ url_for('admin_logs') }}" <a
class="menu-item {% if request.endpoint == 'admin_logs' %}active{% endif %}"> href="{{ url_for('admin_logs') }}"
class="menu-item {% if request.endpoint == 'admin_logs' %}active{% endif %}"
>
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
<span class="menu-text">System Logs</span> <span class="menu-text">System Logs</span>
</a> </a>
@@ -137,7 +144,9 @@
<span class="hamburger-line"></span> <span class="hamburger-line"></span>
<span class="hamburger-line"></span> <span class="hamburger-line"></span>
</button> </button>
<h1 class="page-title">{% block page_title %}QR Code Management System{% endblock %}</h1> <h1 class="page-title">
{% block page_title %}{{ COMPANY_NAME }}{% endblock %}
</h1>
</div> </div>
<div class="header-right"> <div class="header-right">
@@ -172,9 +181,7 @@
{% endif %} {% endwith %} {% endif %} {% endwith %}
<!-- Page Content --> <!-- Page Content -->
<div class="container"> <div class="container">{% block content %}{% endblock %}</div>
{% block content %}{% endblock %}
</div>
</main> </main>
<!-- Footer --> <!-- Footer -->
+299 -177
View File
@@ -1,8 +1,9 @@
{% extends "base_authenticated.html" %} {% extends "base_authenticated.html" %} {% block title %}Dashboard - QR Code
{% block title %}Dashboard - QR Code Management{% endblock %} Management{% endblock %} {% block extra_head %}
<link
{% block extra_head %} rel="stylesheet"
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> href="{{ url_for('static', filename='css/dashboard.css') }}"
/>
<style> <style>
/* Project-Centric Dashboard Styles */ /* Project-Centric Dashboard Styles */
.projects-dashboard { .projects-dashboard {
@@ -770,7 +771,7 @@
.qr-preview-large::after, .qr-preview-large::after,
.qr-preview-compact::after { .qr-preview-compact::after {
content: '🔍'; content: "🔍";
position: absolute; position: absolute;
top: 2px; top: 2px;
right: 2px; right: 2px;
@@ -794,8 +795,12 @@
/* Animations */ /* Animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideIn { @keyframes slideIn {
@@ -957,9 +962,7 @@
} }
} }
</style> </style>
{% endblock %} {% endblock %} {% block content %}
{% block content %}
<div class="dashboard-container"> <div class="dashboard-container">
<!-- Dashboard Header --> <!-- Dashboard Header -->
<div class="dashboard-header"> <div class="dashboard-header">
@@ -968,87 +971,87 @@
<p>Manage your QR codes organized by projects</p> <p>Manage your QR codes organized by projects</p>
<div class="user-info-badge"> <div class="user-info-badge">
<div class="role-indicator {{ session.role }}"> <div class="role-indicator {{ session.role }}">
<i class="fas {{ 'fa-crown' if session.role == 'admin' else 'fa-user' }}"></i> <i
class="fas {{ 'fa-crown' if session.role == 'admin' else 'fa-user' }}"
></i>
{{ session.role.title() }} {{ session.role.title() }}
</div> </div>
{% if session.last_login_date %} {% if session.last_login_date %}
<div class="last-login"> <div class="last-login">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
Last login: {{ session.last_login_date.strftime('%b %d, %Y at %H:%M') }} Last login: {{ session.last_login_date.strftime('%b %d, %Y at %H:%M')
}}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="quick-actions">
<a href="{{ url_for('create_qr_code') }}" class="btn btn-primary btn-lg">
<i class="fas fa-plus"></i>
Create QR Code
</a>
{% if session.role == 'admin' %}
<a href="{{ url_for('users') }}" class="btn btn-secondary btn-lg">
<i class="fas fa-users"></i>
Manage Users
</a>
{% endif %}
</div>
</div>
<!-- Statistics Section --> <!-- QR Statistics in Header -->
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<div class="stats-section"> <div class="header-stats">
<div class="stats-grid"> <div class="header-stats-grid">
<div class="stat-card primary"> <div class="header-stat-card primary">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ qr_codes|length }}</h3> <h3>{{ qr_codes|length }}</h3>
<p>Total QR Codes</p> <p>Total QR Codes</p>
</div> </div>
</div> </div>
<div class="stat-card success"> <div class="header-stat-card success">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ qr_codes|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ qr_codes|selectattr('active_status', 'equalto',
True)|list|length }}
</h3>
<p>Active QR Codes</p> <p>Active QR Codes</p>
</div> </div>
</div> </div>
<div class="stat-card danger"> <div class="header-stat-card danger">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-pause-circle"></i> <i class="fas fa-pause-circle"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ qr_codes|selectattr('active_status', 'equalto', False)|list|length }}</h3> <h3>
{{ qr_codes|selectattr('active_status', 'equalto',
False)|list|length }}
</h3>
<p>Inactive QR Codes</p> <p>Inactive QR Codes</p>
</div> </div>
</div> </div>
<div class="stat-card warning"> <div class="header-stat-card info">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ projects|length }}</h3> <h3>{{ projects|length if projects else 0 }}</h3>
<p>Total Projects</p> <p>Total Projects</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
<!-- Projects Dashboard --> <!-- Projects Dashboard -->
{% if projects %} {% if projects %}
<div class="projects-dashboard"> <div class="projects-dashboard">
{% for project in projects %} {% for project in projects %} {% set project_qr_codes =
{% set project_qr_codes = qr_codes|selectattr('project_id', 'equalto', project.id)|list %} qr_codes|selectattr('project_id', 'equalto', project.id)|list %}
<div class="project-list-item"> <div class="project-list-item">
<!-- Project Header --> <!-- Project Header -->
<div class="project-header" onclick="toggleProject({{ project.id }})"> <div class="project-header" onclick="toggleProject({{ project.id }})">
<div class="project-info"> <div class="project-info">
<div class="project-icon {{ 'inactive' if not project.active_status }}"> <div
<i class="fas fa-folder{{ '-open' if project.active_status else '' }}"></i> class="project-icon {{ 'inactive' if not project.active_status }}"
>
<i
class="fas fa-folder{{ '-open' if project.active_status else '' }}"
></i>
</div> </div>
<div class="project-details"> <div class="project-details">
<h3 class="project-name">{{ project.name }}</h3> <h3 class="project-name">{{ project.name }}</h3>
@@ -1062,9 +1065,14 @@
<div class="project-stats"> <div class="project-stats">
<div class="project-qr-count"> <div class="project-qr-count">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
<span>{{ project_qr_codes|length }} QR Code{{ 's' if project_qr_codes|length != 1 else '' }}</span> <span
>{{ project_qr_codes|length }} QR Code{{ 's' if
project_qr_codes|length != 1 else '' }}</span
>
</div> </div>
<span class="project-status {{ 'active' if project.active_status else 'inactive' }}"> <span
class="project-status {{ 'active' if project.active_status else 'inactive' }}"
>
{{ 'Active' if project.active_status else 'Inactive' }} {{ 'Active' if project.active_status else 'Inactive' }}
</span> </span>
</div> </div>
@@ -1079,18 +1087,26 @@
{% if project_qr_codes %} {% if project_qr_codes %}
<div class="project-qr-grid"> <div class="project-qr-grid">
{% for qr in project_qr_codes %} {% for qr in project_qr_codes %}
<div class="qr-card {{ 'inactive' if not qr.active_status }}" <div
class="qr-card {{ 'inactive' if not qr.active_status }}"
data-qr-id="{{ qr.id }}" data-qr-id="{{ qr.id }}"
data-qr-name="{{ qr.name }}" data-qr-name="{{ qr.name }}"
data-qr-location="{{ qr.location }}" data-qr-location="{{ qr.location }}"
data-qr-address="{{ qr.location_address }}" data-qr-address="{{ qr.location_address }}"
data-qr-event="{{ qr.location_event }}" data-qr-event="{{ qr.location_event }}"
data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}"
onclick="openQRModalFromData(this)"> onclick="openQRModalFromData(this)"
>
<div class="qr-card-layout"> <div class="qr-card-layout">
<div class="qr-preview-large" onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"> <div
<img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy"> class="qr-preview-large"
onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"
>
<img
src="data:image/png;base64,{{ qr.qr_code_image }}"
alt="QR Code for {{ qr.name }}"
loading="lazy"
/>
</div> </div>
<div class="qr-details"> <div class="qr-details">
@@ -1105,34 +1121,51 @@
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
<span>{{ qr.location_address }}</span> <span>{{ qr.location_address }}</span>
</div> </div>
{% endif %} {% endif %} {% if qr.qr_url %}
{% if qr.qr_url %}
<div class="qr-detail-item"> <div class="qr-detail-item">
<i class="fas fa-link"></i> <i class="fas fa-link"></i>
<span>{{ qr.qr_url }}</span> <span>{{ qr.qr_url }}</span>
</div> </div>
{% endif %} {% endif %}
<span class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"> <span
<i class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"></i> class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"
>
<i
class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"
></i>
{{ 'Active' if qr.active_status else 'Inactive' }} {{ 'Active' if qr.active_status else 'Inactive' }}
</span> </span>
</div> </div>
</div> </div>
<div class="qr-card-actions"> <div class="qr-card-actions">
<button class="qr-action-btn download" onclick="event.stopPropagation(); downloadQRFromCard(this)" title="Download QR Code"> <button
class="qr-action-btn download"
onclick="event.stopPropagation(); downloadQRFromCard(this)"
title="Download QR Code"
>
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</button> </button>
<a href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" class="qr-action-btn edit" onclick="event.stopPropagation()" title="Edit QR Code"> <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit"
onclick="event.stopPropagation()"
title="Edit QR Code"
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<button class="qr-action-btn toggle" onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})" title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"> <button
<i class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"></i> class="qr-action-btn toggle"
onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})"
title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"
>
<i
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
></i>
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -1159,31 +1192,42 @@
{% endif %} {% endif %}
<!-- Unassigned QR Codes Section --> <!-- Unassigned QR Codes Section -->
{% set unassigned_qr_codes = qr_codes|selectattr('project_id', 'equalto', None)|list %} {% set unassigned_qr_codes = qr_codes|selectattr('project_id', 'equalto',
{% if unassigned_qr_codes %} None)|list %} {% if unassigned_qr_codes %}
<div class="unassigned-qr-section"> <div class="unassigned-qr-section">
<div class="unassigned-header"> <div class="unassigned-header">
<h3 class="unassigned-title"> <h3 class="unassigned-title">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
Unassigned QR Codes Unassigned QR Codes
</h3> </h3>
<span class="unassigned-count">{{ unassigned_qr_codes|length }} QR Code{{ 's' if unassigned_qr_codes|length != 1 else '' }}</span> <span class="unassigned-count"
>{{ unassigned_qr_codes|length }} QR Code{{ 's' if
unassigned_qr_codes|length != 1 else '' }}</span
>
</div> </div>
<div class="project-qr-grid"> <div class="project-qr-grid">
{% for qr in unassigned_qr_codes %} {% for qr in unassigned_qr_codes %}
<div class="qr-card {{ 'inactive' if not qr.active_status }}" <div
class="qr-card {{ 'inactive' if not qr.active_status }}"
data-qr-id="{{ qr.id }}" data-qr-id="{{ qr.id }}"
data-qr-name="{{ qr.name }}" data-qr-name="{{ qr.name }}"
data-qr-location="{{ qr.location }}" data-qr-location="{{ qr.location }}"
data-qr-address="{{ qr.location_address }}" data-qr-address="{{ qr.location_address }}"
data-qr-event="{{ qr.location_event }}" data-qr-event="{{ qr.location_event }}"
data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}"
onclick="openQRModalFromData(this)"> onclick="openQRModalFromData(this)"
>
<div class="qr-card-compact"> <div class="qr-card-compact">
<div class="qr-preview-compact" onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"> <div
<img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy"> class="qr-preview-compact"
onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"
>
<img
src="data:image/png;base64,{{ qr.qr_code_image }}"
alt="QR Code for {{ qr.name }}"
loading="lazy"
/>
</div> </div>
<div class="qr-info-compact"> <div class="qr-info-compact">
@@ -1199,36 +1243,56 @@
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
<span>{{ qr.location_address }}</span> <span>{{ qr.location_address }}</span>
</div> </div>
{% endif %} {% endif %} {% if qr.qr_url %}
{% if qr.qr_url %}
<div class="qr-detail-item"> <div class="qr-detail-item">
<i class="fas fa-link"></i> <i class="fas fa-link"></i>
<span class="qr-url-short">{{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif %}</span> <span class="qr-url-short"
>{{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif
%}</span
>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="qr-status-compact"> <div class="qr-status-compact">
<span class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"> <span
<i class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"></i> class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"
>
<i
class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"
></i>
{{ 'Active' if qr.active_status else 'Inactive' }} {{ 'Active' if qr.active_status else 'Inactive' }}
</span> </span>
</div> </div>
<div class="qr-actions-compact"> <div class="qr-actions-compact">
<button class="qr-action-btn download" onclick="event.stopPropagation(); downloadQRFromCard(this)" title="Download QR Code"> <button
class="qr-action-btn download"
onclick="event.stopPropagation(); downloadQRFromCard(this)"
title="Download QR Code"
>
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</button> </button>
<a href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" class="qr-action-btn edit" onclick="event.stopPropagation()" title="Edit QR Code"> <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit"
onclick="event.stopPropagation()"
title="Edit QR Code"
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<button class="qr-action-btn toggle" onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})" title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"> <button
<i class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"></i> class="qr-action-btn toggle"
onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})"
title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"
>
<i
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
></i>
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -1246,8 +1310,13 @@
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
</div> </div>
<h3>Welcome to Your QR Code Dashboard</h3> <h3>Welcome to Your QR Code Dashboard</h3>
<p>Get started by creating your first project and QR codes to organize your digital assets effectively.</p> <p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;"> Get started by creating your first project and QR codes to organize your
digital assets effectively.
</p>
<div
style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap"
>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('create_project') }}" class="btn btn-primary"> <a href="{{ url_for('create_project') }}" class="btn btn-primary">
<i class="fas fa-folder-plus"></i> <i class="fas fa-folder-plus"></i>
@@ -1269,7 +1338,7 @@
<button class="lightbox-close" onclick="closeImageLightbox()"> <button class="lightbox-close" onclick="closeImageLightbox()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
<img id="lightboxImage" src="" alt="" class="lightbox-image"> <img id="lightboxImage" src="" alt="" class="lightbox-image" />
<div class="lightbox-info" id="lightboxInfo"></div> <div class="lightbox-info" id="lightboxInfo"></div>
<div class="lightbox-hint">Click anywhere outside the image to close</div> <div class="lightbox-hint">Click anywhere outside the image to close</div>
</div> </div>
@@ -1287,7 +1356,7 @@
<div class="modal-body qr-modal-body"> <div class="modal-body qr-modal-body">
<div class="qr-modal-layout"> <div class="qr-modal-layout">
<div class="qr-modal-image"> <div class="qr-modal-image">
<img id="modalQRImage" src="" alt="QR Code"> <img id="modalQRImage" src="" alt="QR Code" />
</div> </div>
<div class="qr-modal-info"> <div class="qr-modal-info">
@@ -1358,7 +1427,7 @@ class ProjectDashboardManager {
} }
initialize() { initialize() {
const saved = localStorage.getItem('expandedProjects'); const saved = localStorage.getItem("expandedProjects");
if (saved) { if (saved) {
this.expandedProjects = new Set(JSON.parse(saved)); this.expandedProjects = new Set(JSON.parse(saved));
this.restoreProjectStates(); this.restoreProjectStates();
@@ -1366,7 +1435,7 @@ class ProjectDashboardManager {
} }
restoreProjectStates() { restoreProjectStates() {
this.expandedProjects.forEach(projectId => { this.expandedProjects.forEach((projectId) => {
this.expandProject(projectId, false); this.expandProject(projectId, false);
}); });
} }
@@ -1386,19 +1455,19 @@ class ProjectDashboardManager {
expandProject(projectId, animate = true) { expandProject(projectId, animate = true) {
const projectQR = document.getElementById(`project-qr-${projectId}`); const projectQR = document.getElementById(`project-qr-${projectId}`);
const toggle = document.getElementById(`toggle-${projectId}`); const toggle = document.getElementById(`toggle-${projectId}`);
const header = toggle?.closest('.project-header'); const header = toggle?.closest(".project-header");
if (projectQR && toggle) { if (projectQR && toggle) {
projectQR.classList.add('expanded'); projectQR.classList.add("expanded");
toggle.classList.add('expanded'); toggle.classList.add("expanded");
header?.classList.add('expanded'); header?.classList.add("expanded");
this.expandedProjects.add(projectId); this.expandedProjects.add(projectId);
if (animate) { if (animate) {
setTimeout(() => { setTimeout(() => {
projectQR.scrollIntoView({ projectQR.scrollIntoView({
behavior: 'smooth', behavior: "smooth",
block: 'nearest' block: "nearest",
}); });
}, 200); }, 200);
} }
@@ -1408,50 +1477,55 @@ class ProjectDashboardManager {
collapseProject(projectId) { collapseProject(projectId) {
const projectQR = document.getElementById(`project-qr-${projectId}`); const projectQR = document.getElementById(`project-qr-${projectId}`);
const toggle = document.getElementById(`toggle-${projectId}`); const toggle = document.getElementById(`toggle-${projectId}`);
const header = toggle?.closest('.project-header'); const header = toggle?.closest(".project-header");
if (projectQR && toggle) { if (projectQR && toggle) {
projectQR.classList.remove('expanded'); projectQR.classList.remove("expanded");
toggle.classList.remove('expanded'); toggle.classList.remove("expanded");
header?.classList.remove('expanded'); header?.classList.remove("expanded");
this.expandedProjects.delete(projectId); this.expandedProjects.delete(projectId);
} }
} }
saveExpandedState() { saveExpandedState() {
localStorage.setItem('expandedProjects', JSON.stringify([...this.expandedProjects])); localStorage.setItem(
"expandedProjects",
JSON.stringify([...this.expandedProjects])
);
} }
async toggleQRCodeStatus(qrId) { async toggleQRCodeStatus(qrId) {
try { try {
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, { const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'X-Requested-With': 'XMLHttpRequest' "X-Requested-With": "XMLHttpRequest",
} },
}); });
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
if (window.showToast) { if (window.showToast) {
window.showToast(result.message, 'success'); window.showToast(result.message, "success");
} }
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 1000); }, 1000);
} else { } else {
throw new Error(result.message || 'Failed to toggle QR code status'); throw new Error(
result.message || "Failed to toggle QR code status"
);
} }
} else { } else {
throw new Error('Failed to toggle QR code status'); throw new Error("Failed to toggle QR code status");
} }
} catch (error) { } catch (error) {
console.error('Toggle status failed:', error); console.error("Toggle status failed:", error);
if (window.showToast) { if (window.showToast) {
window.showToast('Failed to update QR code status', 'error'); window.showToast("Failed to update QR code status", "error");
} }
} }
} }
@@ -1459,40 +1533,42 @@ class ProjectDashboardManager {
openQRModalFromData(element) { openQRModalFromData(element) {
const qrData = { const qrData = {
name: element.dataset.qrName, name: element.dataset.qrName,
image: element.querySelector('img').src, image: element.querySelector("img").src,
location: element.dataset.qrLocation, location: element.dataset.qrLocation,
address: element.dataset.qrAddress, address: element.dataset.qrAddress,
event: element.dataset.qrEvent, event: element.dataset.qrEvent,
qr_url: element.dataset.qrUrl qr_url: element.dataset.qrUrl,
}; };
this.openQRModal(qrData); this.openQRModal(qrData);
} }
openQRModal(qrData) { openQRModal(qrData) {
const modal = document.getElementById('qrModal'); const modal = document.getElementById("qrModal");
const modalImage = document.getElementById('modalQRImage'); const modalImage = document.getElementById("modalQRImage");
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById("modalTitle");
const modalQRName = document.getElementById('modalQRName'); const modalQRName = document.getElementById("modalQRName");
const modalQRLocation = document.getElementById('modalQRLocation'); const modalQRLocation = document.getElementById("modalQRLocation");
const modalQRAddress = document.getElementById('modalQRAddress'); const modalQRAddress = document.getElementById("modalQRAddress");
const modalQREvent = document.getElementById('modalQREvent'); const modalQREvent = document.getElementById("modalQREvent");
const modalQRDestination = document.getElementById('modalQRDestination'); const modalQRDestination = document.getElementById("modalQRDestination");
if (modal && modalImage && modalTitle) { if (modal && modalImage && modalTitle) {
modalTitle.textContent = `QR Code: ${qrData.name}`; modalTitle.textContent = `QR Code: ${qrData.name}`;
modalImage.src = qrData.image; modalImage.src = qrData.image;
modalImage.alt = `QR Code for ${qrData.name}`; modalImage.alt = `QR Code for ${qrData.name}`;
if (modalQRName) modalQRName.textContent = qrData.name || '-'; if (modalQRName) modalQRName.textContent = qrData.name || "-";
if (modalQRLocation) modalQRLocation.textContent = qrData.location || '-'; if (modalQRLocation)
if (modalQRAddress) modalQRAddress.textContent = qrData.address || '-'; modalQRLocation.textContent = qrData.location || "-";
if (modalQREvent) modalQREvent.textContent = qrData.event || 'No event specified'; if (modalQRAddress) modalQRAddress.textContent = qrData.address || "-";
if (modalQREvent)
modalQREvent.textContent = qrData.event || "No event specified";
if (modalQRDestination && qrData.qr_url) { if (modalQRDestination && qrData.qr_url) {
const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`; const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`;
const linkElement = modalQRDestination.querySelector('a'); const linkElement = modalQRDestination.querySelector("a");
if (linkElement) { if (linkElement) {
linkElement.href = destinationUrl; linkElement.href = destinationUrl;
linkElement.innerHTML = ` linkElement.innerHTML = `
@@ -1501,7 +1577,8 @@ class ProjectDashboardManager {
`; `;
} }
} else if (modalQRDestination) { } else if (modalQRDestination) {
modalQRDestination.innerHTML = '<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>'; modalQRDestination.innerHTML =
'<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>';
} }
this.currentModalQR = { this.currentModalQR = {
@@ -1511,97 +1588,122 @@ class ProjectDashboardManager {
address: qrData.address, address: qrData.address,
event: qrData.event, event: qrData.event,
qr_url: qrData.qr_url, qr_url: qrData.qr_url,
destination_url: qrData.qr_url ? `${window.location.origin}/qr/${qrData.qr_url}` : null destination_url: qrData.qr_url
? `${window.location.origin}/qr/${qrData.qr_url}`
: null,
}; };
modal.style.display = 'flex'; modal.style.display = "flex";
document.addEventListener('keydown', this.handleModalKeydown.bind(this)); document.addEventListener(
"keydown",
this.handleModalKeydown.bind(this)
);
} }
} }
closeQRModal() { closeQRModal() {
const modal = document.getElementById('qrModal'); const modal = document.getElementById("qrModal");
if (modal) { if (modal) {
modal.style.display = 'none'; modal.style.display = "none";
this.currentModalQR = null; this.currentModalQR = null;
document.removeEventListener('keydown', this.handleModalKeydown.bind(this)); document.removeEventListener(
"keydown",
this.handleModalKeydown.bind(this)
);
} }
} }
handleModalKeydown(event) { handleModalKeydown(event) {
if (event.key === 'Escape') { if (event.key === "Escape") {
this.closeQRModal(); this.closeQRModal();
} }
} }
downloadQRFromCard(button) { downloadQRFromCard(button) {
const qrCard = button.closest('.qr-card'); const qrCard = button.closest(".qr-card");
const img = qrCard.querySelector('img'); const img = qrCard.querySelector("img");
const qrName = qrCard.dataset.qrName; const qrName = qrCard.dataset.qrName;
if (img && img.src) { if (img && img.src) {
const base64Data = img.src.split('base64,')[1]; const base64Data = img.src.split("base64,")[1];
this.downloadQR(base64Data, qrName); this.downloadQR(base64Data, qrName);
} }
} }
downloadQR(base64Image, filename) { downloadQR(base64Image, filename) {
try { try {
const base64Data = base64Image.includes('base64,') const base64Data = base64Image.includes("base64,")
? base64Image.split('base64,')[1] ? base64Image.split("base64,")[1]
: base64Image; : base64Image;
const link = document.createElement('a'); const link = document.createElement("a");
link.href = `data:image/png;base64,${base64Data}`; link.href = `data:image/png;base64,${base64Data}`;
link.download = `${filename.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_qr_code.png`; link.download = `${filename
.replace(/[^a-z0-9]/gi, "_")
.toLowerCase()}_qr_code.png`;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
if (window.showToast) { if (window.showToast) {
window.showToast('QR code downloaded successfully!', 'success'); window.showToast("QR code downloaded successfully!", "success");
} }
} catch (error) { } catch (error) {
console.error('Download error:', error); console.error("Download error:", error);
if (window.showToast) { if (window.showToast) {
window.showToast('Failed to download QR code', 'error'); window.showToast("Failed to download QR code", "error");
} }
} }
} }
downloadModalQR() { downloadModalQR() {
if (this.currentModalQR) { if (this.currentModalQR) {
const base64Data = this.currentModalQR.image.split('base64,')[1]; const base64Data = this.currentModalQR.image.split("base64,")[1];
this.downloadQR(base64Data, this.currentModalQR.name); this.downloadQR(base64Data, this.currentModalQR.name);
} }
} }
copyModalQRData() { copyModalQRData() {
if (this.currentModalQR) { if (this.currentModalQR) {
const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${this.currentModalQR.location}\nAddress: ${this.currentModalQR.address}\nEvent: ${this.currentModalQR.event}${this.currentModalQR.destination_url ? `\nQR Link: ${this.currentModalQR.destination_url}` : ''}`; const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${
this.currentModalQR.location
}\nAddress: ${this.currentModalQR.address}\nEvent: ${
this.currentModalQR.event
}${
this.currentModalQR.destination_url
? `\nQR Link: ${this.currentModalQR.destination_url}`
: ""
}`;
navigator.clipboard.writeText(data) navigator.clipboard
.writeText(data)
.then(() => { .then(() => {
if (window.showToast) { if (window.showToast) {
window.showToast('QR code information copied to clipboard!', 'success'); window.showToast(
"QR code information copied to clipboard!",
"success"
);
} }
}) })
.catch(() => { .catch(() => {
const textArea = document.createElement('textarea'); const textArea = document.createElement("textarea");
textArea.value = data; textArea.value = data;
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.select(); textArea.select();
try { try {
document.execCommand('copy'); document.execCommand("copy");
if (window.showToast) { if (window.showToast) {
window.showToast('QR code information copied to clipboard!', 'success'); window.showToast(
"QR code information copied to clipboard!",
"success"
);
} }
} catch (err) { } catch (err) {
if (window.showToast) { if (window.showToast) {
window.showToast('Failed to copy to clipboard', 'error'); window.showToast("Failed to copy to clipboard", "error");
} }
} }
document.body.removeChild(textArea); document.body.removeChild(textArea);
@@ -1611,68 +1713,83 @@ class ProjectDashboardManager {
copyQRDestination() { copyQRDestination() {
if (this.currentModalQR && this.currentModalQR.destination_url) { if (this.currentModalQR && this.currentModalQR.destination_url) {
navigator.clipboard.writeText(this.currentModalQR.destination_url) navigator.clipboard
.writeText(this.currentModalQR.destination_url)
.then(() => { .then(() => {
if (window.showToast) { if (window.showToast) {
window.showToast('QR destination link copied to clipboard!', 'success'); window.showToast(
"QR destination link copied to clipboard!",
"success"
);
} }
}) })
.catch(() => { .catch(() => {
if (window.showToast) { if (window.showToast) {
window.showToast('Failed to copy QR link', 'error'); window.showToast("Failed to copy QR link", "error");
} }
}); });
} else { } else {
if (window.showToast) { if (window.showToast) {
window.showToast('No QR destination link available', 'warning'); window.showToast("No QR destination link available", "warning");
} }
} }
} }
// Image Lightbox Functions // Image Lightbox Functions
openImageLightbox(previewElement, qrName) { openImageLightbox(previewElement, qrName) {
console.log('Opening lightbox for:', qrName); // Debug log console.log("Opening lightbox for:", qrName); // Debug log
const img = previewElement.querySelector('img'); const img = previewElement.querySelector("img");
if (img && img.src) { if (img && img.src) {
const lightbox = document.getElementById('imageLightbox'); const lightbox = document.getElementById("imageLightbox");
const lightboxImage = document.getElementById('lightboxImage'); const lightboxImage = document.getElementById("lightboxImage");
const lightboxInfo = document.getElementById('lightboxInfo'); const lightboxInfo = document.getElementById("lightboxInfo");
console.log('Lightbox elements found:', !!lightbox, !!lightboxImage, !!lightboxInfo); // Debug log console.log(
"Lightbox elements found:",
!!lightbox,
!!lightboxImage,
!!lightboxInfo
); // Debug log
if (lightbox && lightboxImage && lightboxInfo) { if (lightbox && lightboxImage && lightboxInfo) {
lightboxImage.src = img.src; lightboxImage.src = img.src;
lightboxImage.alt = img.alt; lightboxImage.alt = img.alt;
lightboxInfo.textContent = `QR Code: ${qrName}`; lightboxInfo.textContent = `QR Code: ${qrName}`;
lightbox.style.display = 'flex'; lightbox.style.display = "flex";
console.log('Lightbox should be visible now'); // Debug log console.log("Lightbox should be visible now"); // Debug log
// Add keyboard listener for ESC key // Add keyboard listener for ESC key
document.addEventListener('keydown', this.handleLightboxKeydown.bind(this)); document.addEventListener(
"keydown",
this.handleLightboxKeydown.bind(this)
);
} else { } else {
console.error('Lightbox elements not found'); console.error("Lightbox elements not found");
} }
} else { } else {
console.error('Image element not found or no src'); console.error("Image element not found or no src");
} }
} }
closeImageLightbox() { closeImageLightbox() {
console.log('Closing lightbox'); // Debug log console.log("Closing lightbox"); // Debug log
const lightbox = document.getElementById('imageLightbox'); const lightbox = document.getElementById("imageLightbox");
if (lightbox) { if (lightbox) {
lightbox.style.display = 'none'; lightbox.style.display = "none";
// Remove keyboard listener // Remove keyboard listener
document.removeEventListener('keydown', this.handleLightboxKeydown.bind(this)); document.removeEventListener(
"keydown",
this.handleLightboxKeydown.bind(this)
);
} }
} }
handleLightboxKeydown(event) { handleLightboxKeydown(event) {
if (event.key === 'Escape') { if (event.key === "Escape") {
this.closeImageLightbox(); this.closeImageLightbox();
} }
} }
@@ -1680,21 +1797,26 @@ class ProjectDashboardManager {
let dashboardManager; let dashboardManager;
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
dashboardManager = new ProjectDashboardManager(); dashboardManager = new ProjectDashboardManager();
window.toggleProject = (projectId) => dashboardManager.toggleProject(projectId); window.toggleProject = (projectId) =>
window.toggleQRCodeStatus = (qrId) => dashboardManager.toggleQRCodeStatus(qrId); dashboardManager.toggleProject(projectId);
window.openQRModalFromData = (element) => dashboardManager.openQRModalFromData(element); window.toggleQRCodeStatus = (qrId) =>
dashboardManager.toggleQRCodeStatus(qrId);
window.openQRModalFromData = (element) =>
dashboardManager.openQRModalFromData(element);
window.closeQRModal = () => dashboardManager.closeQRModal(); window.closeQRModal = () => dashboardManager.closeQRModal();
window.downloadModalQR = () => dashboardManager.downloadModalQR(); window.downloadModalQR = () => dashboardManager.downloadModalQR();
window.copyModalQRData = () => dashboardManager.copyModalQRData(); window.copyModalQRData = () => dashboardManager.copyModalQRData();
window.copyQRDestination = () => dashboardManager.copyQRDestination(); window.copyQRDestination = () => dashboardManager.copyQRDestination();
window.downloadQRFromCard = (button) => dashboardManager.downloadQRFromCard(button); window.downloadQRFromCard = (button) =>
window.openImageLightbox = (element, qrName) => dashboardManager.openImageLightbox(element, qrName); dashboardManager.downloadQRFromCard(button);
window.openImageLightbox = (element, qrName) =>
dashboardManager.openImageLightbox(element, qrName);
window.closeImageLightbox = () => dashboardManager.closeImageLightbox(); window.closeImageLightbox = () => dashboardManager.closeImageLightbox();
console.log('Project Dashboard initialized successfully'); console.log("Project Dashboard initialized successfully");
}); });
</script> </script>
{% endblock %} {% endblock %}