Clean up dashboard

This commit is contained in:
Nguyen Ngo
2025-08-15 17:18:12 -04:00
parent 6b40637da3
commit 8dd7f82bdd
+241 -165
View File
@@ -173,11 +173,12 @@
.project-qr-grid { .project-qr-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: 1fr;
gap: var(--spacing-4); gap: var(--spacing-3);
padding: var(--spacing-6); padding: var(--spacing-6);
} }
/* QR Card Base Styles */
.qr-card { .qr-card {
background: var(--white); background: var(--white);
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
@@ -199,39 +200,106 @@
background: linear-gradient(135deg, var(--white), #f8fafc); background: linear-gradient(135deg, var(--white), #f8fafc);
} }
.qr-card-header { /* Compact QR Card Layout for Unassigned Cards */
.qr-card-compact {
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: var(--spacing-3); gap: var(--spacing-4);
margin-bottom: var(--spacing-3); width: 100%;
padding: var(--spacing-3);
} }
.qr-preview { .qr-preview-compact {
width: 60px; width: 70px;
height: 60px; height: 70px;
flex-shrink: 0; flex-shrink: 0;
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
border: 2px solid var(--gray-200); border: 2px solid var(--gray-200);
transition: var(--transition); transition: var(--transition);
cursor: pointer;
} }
.qr-preview img { .qr-preview-compact img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
.qr-card:hover .qr-preview { .qr-card:hover .qr-preview-compact {
border-color: var(--primary-color); border-color: var(--primary-color);
} }
.qr-info { .qr-info-compact {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.qr-name-compact {
font-size: var(--font-size-base);
font-weight: 600;
color: var(--gray-900);
margin: 0 0 var(--spacing-1) 0;
line-height: 1.4;
}
.qr-details-row {
display: flex;
gap: var(--spacing-4);
flex-wrap: wrap;
}
.qr-url-short {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.qr-status-compact {
flex-shrink: 0;
margin: 0 var(--spacing-3);
}
.qr-actions-compact {
display: flex;
gap: var(--spacing-1);
flex-shrink: 0;
}
/* Standard QR Card Layout for Project Cards */
.qr-card-layout {
display: flex;
gap: var(--spacing-4);
margin-bottom: var(--spacing-3);
}
.qr-preview-large {
width: 100px;
height: 100px;
flex-shrink: 0;
border-radius: var(--radius-md);
overflow: hidden;
border: 2px solid var(--gray-200);
transition: var(--transition);
}
.qr-preview-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
.qr-card:hover .qr-preview-large {
border-color: var(--primary-color);
}
.qr-details {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.qr-name { .qr-name {
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: 600; font-weight: 600;
@@ -240,13 +308,27 @@
line-height: 1.4; line-height: 1.4;
} }
.qr-location { .qr-detail-item {
color: var(--gray-600);
font-size: var(--font-size-sm);
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-1); gap: 0.5rem;
margin-bottom: var(--spacing-2); font-size: var(--font-size-sm);
color: var(--gray-600);
min-width: 0;
}
.qr-detail-item i {
font-size: 0.75rem;
width: auto;
flex-shrink: 0;
color: var(--gray-400);
}
.qr-detail-item span {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.qr-status-badge { .qr-status-badge {
@@ -269,18 +351,12 @@
color: #dc2626; color: #dc2626;
} }
.qr-actions { .qr-card-actions {
position: absolute;
top: var(--spacing-3);
right: var(--spacing-3);
display: flex; display: flex;
gap: var(--spacing-1); gap: var(--spacing-2);
opacity: 0; justify-content: flex-start;
transition: var(--transition); padding-top: var(--spacing-3);
} border-top: 1px solid var(--gray-200);
.qr-card:hover .qr-actions {
opacity: 1;
} }
.qr-action-btn { .qr-action-btn {
@@ -317,16 +393,6 @@
color: var(--white); color: var(--white);
} }
.qr-action-btn.delete {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
.qr-action-btn.delete:hover {
background: #dc2626;
color: var(--white);
}
.qr-action-btn.toggle { .qr-action-btn.toggle {
background: rgba(245, 158, 11, 0.1); background: rgba(245, 158, 11, 0.1);
color: #d97706; color: #d97706;
@@ -337,6 +403,7 @@
color: var(--white); color: var(--white);
} }
/* Empty States */
.empty-project-qr { .empty-project-qr {
text-align: center; text-align: center;
padding: var(--spacing-8) var(--spacing-4); padding: var(--spacing-8) var(--spacing-4);
@@ -368,6 +435,41 @@
margin-bottom: var(--spacing-4); margin-bottom: var(--spacing-4);
} }
.dashboard-empty {
text-align: center;
padding: 4rem 2rem;
color: var(--gray-600);
}
.dashboard-empty .empty-icon {
width: 120px;
height: 120px;
margin: 0 auto 2rem;
background: var(--gray-100);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--gray-400);
}
.dashboard-empty h3 {
font-size: 2rem;
font-weight: 600;
color: var(--gray-900);
margin-bottom: 1rem;
}
.dashboard-empty p {
font-size: 1.125rem;
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
/* Unassigned QR Section */
.unassigned-qr-section { .unassigned-qr-section {
background: var(--white); background: var(--white);
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
@@ -404,6 +506,7 @@
font-weight: 600; font-weight: 600;
} }
/* Modal Styles */
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
@@ -517,26 +620,26 @@
padding-bottom: var(--spacing-2); padding-bottom: var(--spacing-2);
} }
.qr-detail-item { .qr-detail-section .qr-detail-item {
display: grid; display: grid;
grid-template-columns: 120px 1fr; grid-template-columns: 100px 1fr;
gap: var(--spacing-3); gap: var(--spacing-3);
margin-bottom: var(--spacing-4); margin-bottom: var(--spacing-4);
align-items: start; align-items: start;
} }
.qr-detail-item:last-child { .qr-detail-section .qr-detail-item:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.qr-detail-item label { .qr-detail-section .qr-detail-item label {
font-weight: 600; font-weight: 600;
color: var(--gray-700); color: var(--gray-700);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
padding-top: 2px; padding-top: 2px;
} }
.qr-detail-item span { .qr-detail-section .qr-detail-item span {
color: var(--gray-900); color: var(--gray-900);
font-size: var(--font-size-base); font-size: var(--font-size-base);
line-height: 1.5; line-height: 1.5;
@@ -576,40 +679,7 @@
min-width: 140px; min-width: 140px;
} }
.dashboard-empty { /* Animations */
text-align: center;
padding: 4rem 2rem;
color: var(--gray-600);
}
.dashboard-empty .empty-icon {
width: 120px;
height: 120px;
margin: 0 auto 2rem;
background: var(--gray-100);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--gray-400);
}
.dashboard-empty h3 {
font-size: 2rem;
font-weight: 600;
color: var(--gray-900);
margin-bottom: 1rem;
}
.dashboard-empty p {
font-size: 1.125rem;
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }
@@ -626,6 +696,7 @@
} }
} }
/* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.project-header { .project-header {
padding: var(--spacing-3) var(--spacing-4); padding: var(--spacing-3) var(--spacing-4);
@@ -647,18 +718,47 @@
gap: var(--spacing-2); gap: var(--spacing-2);
} }
.project-qr-grid { .qr-card-compact {
grid-template-columns: 1fr; flex-direction: column;
padding: var(--spacing-4); text-align: center;
gap: var(--spacing-3);
} }
.qr-card-header { .qr-info-compact {
width: 100%;
}
.qr-details-row {
flex-direction: column;
gap: var(--spacing-2); gap: var(--spacing-2);
align-items: center;
} }
.qr-preview { .qr-status-compact {
width: 50px; margin: 0;
height: 50px; }
.qr-actions-compact {
justify-content: center;
}
.qr-card-layout {
flex-direction: column;
text-align: center;
}
.qr-preview-large {
align-self: center;
width: 80px;
height: 80px;
}
.qr-details {
align-items: center;
}
.qr-detail-item {
justify-content: center;
} }
.unassigned-header { .unassigned-header {
@@ -691,11 +791,6 @@
height: 240px; height: 240px;
} }
.qr-detail-item {
grid-template-columns: 1fr;
gap: var(--spacing-1);
}
.qr-modal-footer { .qr-modal-footer {
flex-direction: column; flex-direction: column;
} }
@@ -713,13 +808,6 @@
text-overflow: unset; text-overflow: unset;
} }
.qr-actions {
position: static;
opacity: 1;
margin-top: var(--spacing-2);
justify-content: flex-end;
}
.modal-header { .modal-header {
padding: var(--spacing-4); padding: var(--spacing-4);
} }
@@ -872,16 +960,33 @@
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-header">
<div class="qr-preview"> <div class="qr-card-layout">
<div class="qr-preview-large">
<img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy"> <img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy">
</div> </div>
<div class="qr-info">
<div class="qr-details">
<h4 class="qr-name">{{ qr.name }}</h4> <h4 class="qr-name">{{ qr.name }}</h4>
<p class="qr-location"> <div class="qr-detail-item">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
{{ qr.location }} <span>{{ qr.location }}</span>
</p> </div>
{% if qr.location_address %}
<div class="qr-detail-item">
<i class="fas fa-home"></i>
<span>{{ qr.location_address }}</span>
</div>
{% endif %}
{% if qr.qr_url %}
<div class="qr-detail-item">
<i class="fas fa-link"></i>
<span>{{ qr.qr_url }}</span>
</div>
{% endif %}
<span class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"> <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> <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' }}
@@ -889,7 +994,7 @@
</div> </div>
</div> </div>
<div class="qr-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>
@@ -902,10 +1007,6 @@
<button class="qr-action-btn toggle" onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})" title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"> <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> <i class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"></i>
</button> </button>
<button class="qr-action-btn delete" onclick="event.stopPropagation(); deleteQRCode({{ qr.id }}, '{{ qr.name }}')" title="Delete QR Code">
<i class="fas fa-trash"></i>
</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -952,24 +1053,44 @@
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-header">
<div class="qr-preview"> <div class="qr-card-compact">
<div class="qr-preview-compact">
<img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy"> <img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy">
</div> </div>
<div class="qr-info">
<h4 class="qr-name">{{ qr.name }}</h4> <div class="qr-info-compact">
<p class="qr-location"> <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> <i class="fas fa-map-marker-alt"></i>
{{ qr.location }} <span>{{ qr.location }}</span>
</p> </div>
{% if qr.location_address %}
<div class="qr-detail-item">
<i class="fas fa-home"></i>
<span>{{ qr.location_address }}</span>
</div>
{% endif %}
{% 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' }}"> <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> <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 class="qr-actions"> <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>
@@ -982,13 +1103,10 @@
<button class="qr-action-btn toggle" onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})" title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"> <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> <i class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"></i>
</button> </button>
<button class="qr-action-btn delete" onclick="event.stopPropagation(); deleteQRCode({{ qr.id }}, '{{ qr.name }}')" title="Delete QR Code">
<i class="fas fa-trash"></i>
</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -1199,50 +1317,10 @@ class ProjectDashboardManager {
} }
} }
async deleteQRCode(qrId, qrName) {
const confirmMessage = `Are you sure you want to permanently delete the QR code "${qrName}"?\n\nThis action cannot be undone.`;
if (!confirm(confirmMessage)) return;
try {
const response = await fetch(`/qr-codes/${qrId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
if (window.showToast) {
window.showToast(`QR code "${qrName}" deleted successfully`, 'success');
}
const qrCard = document.querySelector(`[data-qr-id="${qrId}"]`);
if (qrCard) {
qrCard.style.transition = 'all 0.3s ease';
qrCard.style.opacity = '0';
qrCard.style.transform = 'translateY(-20px)';
setTimeout(() => {
qrCard.remove();
}, 300);
}
} else {
throw new Error('Failed to delete QR code');
}
} catch (error) {
console.error('Delete failed:', error);
if (window.showToast) {
window.showToast('Failed to delete QR code', 'error');
}
}
}
openQRModalFromData(element) { openQRModalFromData(element) {
const qrData = { const qrData = {
name: element.dataset.qrName, name: element.dataset.qrName,
image: element.querySelector('.qr-preview 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,
@@ -1321,11 +1399,10 @@ class ProjectDashboardManager {
downloadQRFromCard(button) { downloadQRFromCard(button) {
const qrCard = button.closest('.qr-card'); const qrCard = button.closest('.qr-card');
const img = qrCard.querySelector('.qr-preview img'); const img = qrCard.querySelector('img');
const qrName = qrCard.dataset.qrName; const qrName = qrCard.dataset.qrName;
if (img && img.src) { if (img && img.src) {
// Extract base64 data from the image src
const base64Data = img.src.split('base64,')[1]; const base64Data = img.src.split('base64,')[1];
this.downloadQR(base64Data, qrName); this.downloadQR(base64Data, qrName);
} }
@@ -1421,7 +1498,6 @@ document.addEventListener('DOMContentLoaded', function() {
window.toggleProject = (projectId) => dashboardManager.toggleProject(projectId); window.toggleProject = (projectId) => dashboardManager.toggleProject(projectId);
window.toggleQRCodeStatus = (qrId) => dashboardManager.toggleQRCodeStatus(qrId); window.toggleQRCodeStatus = (qrId) => dashboardManager.toggleQRCodeStatus(qrId);
window.deleteQRCode = (qrId, qrName) => dashboardManager.deleteQRCode(qrId, qrName);
window.openQRModalFromData = (element) => dashboardManager.openQRModalFromData(element); window.openQRModalFromData = (element) => dashboardManager.openQRModalFromData(element);
window.closeQRModal = () => dashboardManager.closeQRModal(); window.closeQRModal = () => dashboardManager.closeQRModal();
window.downloadModalQR = () => dashboardManager.downloadModalQR(); window.downloadModalQR = () => dashboardManager.downloadModalQR();