Enhanced dashboard to have search functionality
This commit is contained in:
@@ -1681,21 +1681,50 @@ def logout():
|
|||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
"""Enhanced project-centric dashboard"""
|
"""Enhanced project-centric dashboard with search filters"""
|
||||||
try:
|
try:
|
||||||
user = User.query.get(session['user_id'])
|
user = User.query.get(session['user_id'])
|
||||||
|
|
||||||
# Get ALL QR codes and projects for project-centric view
|
# Get search parameters from URL
|
||||||
qr_codes = QRCode.query.order_by(QRCode.created_date.desc()).all()
|
search_name = request.args.get('search_name', '').strip()
|
||||||
|
search_status = request.args.get('search_status', '').strip()
|
||||||
|
|
||||||
|
# Build QR codes query with filters
|
||||||
|
qr_query = QRCode.query
|
||||||
|
|
||||||
|
# Apply name filter if provided
|
||||||
|
if search_name:
|
||||||
|
qr_query = qr_query.filter(QRCode.name.ilike(f'%{search_name}%'))
|
||||||
|
|
||||||
|
# Apply status filter if provided
|
||||||
|
if search_status == 'active':
|
||||||
|
qr_query = qr_query.filter(QRCode.active_status == True)
|
||||||
|
elif search_status == 'inactive':
|
||||||
|
qr_query = qr_query.filter(QRCode.active_status == False)
|
||||||
|
|
||||||
|
# Execute query
|
||||||
|
qr_codes = qr_query.order_by(QRCode.created_date.desc()).all()
|
||||||
projects = Project.query.order_by(Project.name.asc()).all()
|
projects = Project.query.order_by(Project.name.asc()).all()
|
||||||
|
|
||||||
# Log dashboard access with project info
|
# Log dashboard access with filter info
|
||||||
logger_handler.logger.info(f"User {session['username']} accessed project dashboard: {len(qr_codes)} QR codes, {len(projects)} projects")
|
filter_info = []
|
||||||
|
if search_name:
|
||||||
|
filter_info.append(f"name contains '{search_name}'")
|
||||||
|
if search_status:
|
||||||
|
filter_info.append(f"status is {search_status}")
|
||||||
|
|
||||||
|
log_message = f"User {session['username']} accessed dashboard: {len(qr_codes)} QR codes"
|
||||||
|
if filter_info:
|
||||||
|
log_message += f" (filtered: {', '.join(filter_info)})"
|
||||||
|
|
||||||
|
logger_handler.logger.info(log_message)
|
||||||
|
|
||||||
return render_template('dashboard.html',
|
return render_template('dashboard.html',
|
||||||
user=user,
|
user=user,
|
||||||
qr_codes=qr_codes,
|
qr_codes=qr_codes,
|
||||||
projects=projects)
|
projects=projects,
|
||||||
|
search_name=search_name,
|
||||||
|
search_status=search_status)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger_handler.log_database_error('dashboard_load', e)
|
logger_handler.log_database_error('dashboard_load', e)
|
||||||
@@ -1742,6 +1771,22 @@ def project_qr_codes(project_id):
|
|||||||
flash('Error loading project QR codes. Please try again.', 'error')
|
flash('Error loading project QR codes. Please try again.', 'error')
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
@app.route('/dashboard/search', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def search_qr_codes():
|
||||||
|
"""Search QR codes - redirect to dashboard with filters"""
|
||||||
|
search_name = request.args.get('search_name', '').strip()
|
||||||
|
search_status = request.args.get('search_status', '').strip()
|
||||||
|
|
||||||
|
# Log search activity
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"User {session['username']} searched QR codes: "
|
||||||
|
f"name='{search_name}', status='{search_status}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Redirect to dashboard with search parameters
|
||||||
|
return redirect(url_for('dashboard', search_name=search_name, search_status=search_status))
|
||||||
|
|
||||||
@app.route('/api/dashboard/stats')
|
@app.route('/api/dashboard/stats')
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard_stats_api():
|
def dashboard_stats_api():
|
||||||
|
|||||||
@@ -1337,3 +1337,99 @@
|
|||||||
border-top: 1px solid var(--gray-200);
|
border-top: 1px solid var(--gray-200);
|
||||||
background: var(--gray-50);
|
background: var(--gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
QR Code Search Section
|
||||||
|
============================================ */
|
||||||
|
.search-section {
|
||||||
|
background: var(--white);
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--spacing-6);
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group label {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-700);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input,
|
||||||
|
.search-select {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
transition: var(--transition);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus,
|
||||||
|
.search-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions .btn {
|
||||||
|
min-width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.search-input-group {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-inputs {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group {
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
+273
-13
@@ -981,6 +981,58 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
padding: var(--spacing-4);
|
padding: var(--spacing-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Search Results Banner
|
||||||
|
============================================ */
|
||||||
|
.search-results-banner {
|
||||||
|
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||||
|
border: 1px solid #3b82f6;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-4) var(--spacing-6);
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
color: #1e40af;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-content i {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-content span {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-content .btn-sm {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
height: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-results-banner {
|
||||||
|
padding: var(--spacing-3) var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-content .btn-sm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %} {% block content %}
|
{% endblock %} {% block content %}
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container">
|
||||||
@@ -1056,6 +1108,206 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- QR Code Search Section -->
|
||||||
|
<div class="search-section">
|
||||||
|
<form action="{{ url_for('dashboard') }}" method="GET" class="search-form">
|
||||||
|
<div class="search-inputs">
|
||||||
|
<div class="search-input-group">
|
||||||
|
<label for="search_name">
|
||||||
|
<i class="fas fa-search"></i> Search by Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search_name"
|
||||||
|
name="search_name"
|
||||||
|
placeholder="Enter QR code name..."
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-input-group">
|
||||||
|
<label for="search_status">
|
||||||
|
<i class="fas fa-filter"></i> Filter by Status
|
||||||
|
</label>
|
||||||
|
<select id="search_status" name="search_status" class="search-select">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results Banner and Section -->
|
||||||
|
{% if search_name or search_status %}
|
||||||
|
<div class="search-results-banner">
|
||||||
|
<div class="search-results-content">
|
||||||
|
<i class="fas fa-filter"></i>
|
||||||
|
<span>
|
||||||
|
Showing filtered results
|
||||||
|
{% if search_name %} - <strong>{{ search_status|title }}</strong> only{% endif %}
|
||||||
|
{% if search_status %} - <strong>{{ search_status|title }}</strong> only{% endif %}
|
||||||
|
({{ qr_codes|length }} QR code{{ 's' if qr_codes|length != 1 else '' }} found)
|
||||||
|
</span>
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-sm btn-outline">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
Clear Filters
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results Display -->
|
||||||
|
{% if qr_codes %}
|
||||||
|
<div class="unassigned-qr-section">
|
||||||
|
<div class="unassigned-header">
|
||||||
|
<h3 class="unassigned-title">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
Search Results
|
||||||
|
</h3>
|
||||||
|
<span class="unassigned-count"
|
||||||
|
>{{ qr_codes|length }} QR Code{{ 's' if qr_codes|length != 1 else '' }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-qr-grid">
|
||||||
|
{% for qr in qr_codes %}
|
||||||
|
<div
|
||||||
|
class="qr-card {{ 'inactive' if not qr.active_status }}"
|
||||||
|
data-qr-id="{{ qr.id }}"
|
||||||
|
data-qr-name="{{ qr.name }}"
|
||||||
|
data-qr-location="{{ qr.location }}"
|
||||||
|
data-qr-address="{{ qr.location_address }}"
|
||||||
|
data-qr-event="{{ qr.location_event }}"
|
||||||
|
data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}"
|
||||||
|
onclick="openQRModalFromData(this)"
|
||||||
|
>
|
||||||
|
<div class="qr-card-compact">
|
||||||
|
<div
|
||||||
|
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 class="qr-info-compact">
|
||||||
|
<h4 class="qr-name-compact">{{ qr.name }}</h4>
|
||||||
|
<div class="qr-details-row">
|
||||||
|
<div class="qr-detail-item">
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
<span>{{ qr.location }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if qr.location_address %}
|
||||||
|
<div class="qr-detail-item">
|
||||||
|
<i class="fas fa-home"></i>
|
||||||
|
<span>{{ qr.location_address }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="qr-detail-item qr-checkins-count">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span>{{ get_qr_code_checkin_count(qr.id) }} check-ins</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if qr.qr_url %}
|
||||||
|
<div class="qr-detail-item">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
<span class="qr-url-short">{{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-status-compact">
|
||||||
|
<span
|
||||||
|
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' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-actions-compact">
|
||||||
|
<button
|
||||||
|
class="qr-action-btn copy"
|
||||||
|
onclick="event.stopPropagation(); copyQRUrl({{ qr.id }})"
|
||||||
|
title="Copy QR Code URL"
|
||||||
|
>
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="qr-action-btn link"
|
||||||
|
onclick="event.stopPropagation(); openQRLink({{ qr.id }})"
|
||||||
|
title="Open QR Code Link"
|
||||||
|
>
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="qr-action-btn download"
|
||||||
|
onclick="event.stopPropagation(); downloadQRFromCard(this)"
|
||||||
|
title="Download QR Code"
|
||||||
|
>
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% 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"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- No Search Results -->
|
||||||
|
<div class="dashboard-empty">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</div>
|
||||||
|
<h3>No QR Codes Found</h3>
|
||||||
|
<p>No QR codes match your search criteria. Try adjusting your filters.</p>
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
Clear Filters
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
<!-- Projects Dashboard -->
|
<!-- Projects Dashboard -->
|
||||||
{% if projects %}
|
{% if projects %}
|
||||||
<div class="projects-dashboard">
|
<div class="projects-dashboard">
|
||||||
@@ -1375,8 +1627,24 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
{% if not projects and not qr_codes %}
|
{% if not qr_codes and not projects %}
|
||||||
<div class="dashboard-empty">
|
<div class="dashboard-empty">
|
||||||
|
{% if search_name or search_status %}
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</div>
|
||||||
|
<h3>No QR Codes Found</h3>
|
||||||
|
<p>
|
||||||
|
No QR codes match your search criteria.
|
||||||
|
{% if search_name %}Try a different name{% endif %}
|
||||||
|
{% if search_name and search_status %} or {% endif %}
|
||||||
|
{% if search_status %}try changing the status filter{% endif %}.
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
Clear Filters
|
||||||
|
</a>
|
||||||
|
{% elif not projects %}
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -1385,23 +1653,15 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
Get started by creating your first project and QR codes to organize your
|
Get started by creating your first project and QR codes to organize your
|
||||||
digital assets effectively.
|
digital assets effectively.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<a href="{{ url_for('projects') }}" class="btn btn-primary">
|
||||||
style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap"
|
|
||||||
>
|
|
||||||
{% if session.role == 'admin' %}
|
|
||||||
<a href="{{ url_for('create_project') }}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-folder-plus"></i>
|
|
||||||
Create Project
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ url_for('create_qr_code') }}" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
Create QR Code
|
Create Your First Project
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Image Lightbox -->
|
<!-- Image Lightbox -->
|
||||||
<div id="imageLightbox" class="image-lightbox" onclick="closeImageLightbox()">
|
<div id="imageLightbox" class="image-lightbox" onclick="closeImageLightbox()">
|
||||||
|
|||||||
Reference in New Issue
Block a user