Update dashboard
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 %}
|
||||||
Reference in New Issue
Block a user