diff --git a/app.py b/app.py index b2436d4..aeaf9fc 100644 --- a/app.py +++ b/app.py @@ -70,6 +70,10 @@ class User(db.Model): def has_staff_permissions(self): """Check if user has staff-level permissions (includes new roles)""" return self.role in STAFF_LEVEL_ROLES + + def has_export_permissions(user_role): + """Check if user role has export permissions""" + return user_role in ['admin', 'payroll'] def get_role_display_name(self): """Get user-friendly role name""" @@ -2399,17 +2403,27 @@ def create_qr_code(): 'longitude': address_longitude, 'coordinate_accuracy': coordinate_accuracy } - logger_handler.logger.info(f"User {session['username']} created QR code: {json.dumps(log_data)}") - logger_handler.log_user_action( - f'QR Code created: {name} with event type: {location_event}', - 'create', - additional_info={'location_event': location_event} + qr_data_for_logging = { + 'location': location, + 'location_address': location_address, + 'location_event': location_event, + 'has_coordinates': has_coordinates, + 'latitude': address_latitude, + 'longitude': address_longitude, + 'coordinate_accuracy': coordinate_accuracy + } + + logger_handler.log_qr_code_created( + qr_code_id=new_qr_code.id, + qr_code_name=name, + created_by_user_id=session['user_id'], + qr_data=qr_data_for_logging ) # Success message with coordinates info project_info = f" in project '{project.name}'" if project else "" coord_info = f" with coordinates ({new_qr_code.coordinates_display})" if has_coordinates else "" - + flash(f'QR Code "{name}" created successfully{project_info}{coord_info}! URL: {qr_url}', 'success') return redirect(url_for('dashboard')) @@ -2991,7 +3005,7 @@ def toggle_qr_status_api(qr_id): return redirect(url_for('dashboard')) @app.route('/attendance') -# @admin_required +@login_required def attendance_report(): """Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling""" try: @@ -2999,6 +3013,7 @@ def attendance_report(): # Log attendance report access try: + user_role = session.get('role', 'unknown') logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report") except Exception as log_error: print(f"⚠️ Logging error (non-critical): {log_error}") @@ -3080,7 +3095,7 @@ def attendance_report(): conditions.append("ad.employee_id LIKE :employee") params['employee'] = f"%{employee_filter}%" - # FIXED: Apply project filter using SQL approach (not ORM) + # Apply project filter using SQL approach (not ORM) if project_filter: conditions.append("qc.project_id = :project_id") params['project_id'] = int(project_filter) @@ -3098,9 +3113,14 @@ def attendance_report(): # Execute query query_result = db.session.execute(text(base_query), params) attendance_records = query_result.fetchall() - print(f"✅ Found {len(attendance_records)} attendance records") - + # Log first 3 records to verify QR address data + for i, record in enumerate(attendance_records[:3]): + print(f"📊 Record {i+1}: Employee={record.employee_id}") + print(f" QR Address: {getattr(record, 'qr_address', 'NOT_FOUND')}") + print(f" Check-in Address: {getattr(record, 'checked_in_address', 'NOT_FOUND')}") + print(f" Location Accuracy: {getattr(record, 'location_accuracy', 'NOT_FOUND')}") + # FIXED: Process records to add calculated fields with proper datetime handling processed_records = [] for record in attendance_records: @@ -3264,7 +3284,8 @@ def attendance_report(): project_filter=project_filter, today_date=today_date, current_date_formatted=current_date_formatted, - has_location_accuracy_feature=has_location_accuracy) + has_location_accuracy_feature=has_location_accuracy, + user_role=user_role) except Exception as e: print(f"❌ Error loading attendance report: {e}") @@ -3476,14 +3497,21 @@ def attendance_stats_api(): return jsonify({'error': 'Failed to fetch attendance statistics'}), 500 @app.route('/export-configuration') -@admin_required +@login_required def export_configuration(): """Route to display export configuration page""" try: print("📊 Export configuration route accessed") + # Check if user has export permissions + user_role = session.get('role') + if user_role not in ['admin', 'payroll']: + logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted to access export configuration without permissions") + flash('Access denied. Only administrators and payroll staff can access export configuration.', 'error') + return redirect(url_for('attendance_report')) # Log export configuration access using your existing logger try: + logger_handler.logger.info(f"User {session.get('username', 'unknown')} (role: {user_role}) accessed export configuration") logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page") except Exception as log_error: print(f"⚠️ Logging error (non-critical): {log_error}") @@ -3558,10 +3586,16 @@ def export_configuration(): return redirect(url_for('attendance_report')) @app.route('/generate-excel-export', methods=['POST']) -@admin_required +@login_required def generate_excel_export(): """Generate and download Excel file with selected columns in specified order""" try: + user_role = session.get('role') + if user_role not in ['admin', 'payroll']: + logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized Excel export") + flash('Access denied. Only administrators and payroll staff can export data.', 'error') + return redirect(url_for('attendance_report')) + print("📊 Excel export generation started") # Log export action using your existing logger diff --git a/static/css/attendance.css b/static/css/attendance.css index eba85ac..d9d23de 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -5,7 +5,7 @@ /* Page Container */ .attendance-page { - max-width: 1400px; + max-width: 1600px; margin: 0 auto; padding: var(--spacing-6); min-height: 100vh; @@ -993,40 +993,16 @@ opacity: 0.8; } -/* Location accuracy level colors - based on distance */ -.location-accuracy-badge.accuracy-excellent { - background: var(--success-light); - color: var(--success-color); - border: 1px solid rgba(5, 150, 105, 0.3); -} - +/* Location accuracy level colors - 2-level system based on 0.5 mile threshold */ .location-accuracy-badge.accuracy-accurate { - background: var(--success-light); - color: var(--success-color); - border: 1px solid rgba(5, 150, 105, 0.3); -} - -.location-accuracy-badge.accuracy-good { - background: #ecfdf5; + background: #dcfce7; color: #059669; - border: 1px solid rgba(5, 150, 105, 0.2); -} - -.location-accuracy-badge.accuracy-fair { - background: var(--warning-light); - color: var(--warning-color); - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.location-accuracy-badge.accuracy-poor { - background: var(--danger-light); - color: var(--danger-color); - border: 1px solid rgba(220, 38, 38, 0.3); + border: 1px solid rgba(5, 150, 105, 0.3); } .location-accuracy-badge.accuracy-inaccurate { - background: var(--danger-light); - color: var(--danger-color); + background: #fee2e2; + color: #dc2626; border: 1px solid rgba(220, 38, 38, 0.3); } diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 69a692a..98cd4dc 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -657,16 +657,14 @@ function extractLocationAccuracy(cell) { } function extractLocationAccuracyLevel(cell) { - const text = cell.textContent.toLowerCase(); - if ( - text.includes("high") || - text.includes("excellent") || - text.includes("good") - ) - return "High"; - if (text.includes("medium") || text.includes("fair")) return "Medium"; - if (text.includes("low") || text.includes("poor")) return "Low"; - return "Unknown"; + // Get the numerical accuracy value from the cell + const accuracy = extractLocationAccuracy(cell); + + // Return 2-level accuracy based on 0.5-mile threshold + if (accuracy !== null && accuracy !== undefined) { + return accuracy <= 0.5 ? "accurate" : "inaccurate"; + } + return "unknown"; } function createTableRow(record, displayIndex) { @@ -690,17 +688,17 @@ function createTableRow(record, displayIndex) { }" title="Distance between QR location and check-in location: ${ record.location_accuracy - } miles"> + } miles - ${record.accuracy_level}"> ${record.location_accuracy.toFixed(3)} mi (${record.accuracy_level}) - ` + ` : ` Unknown - `; + `; - // FIXED: Address display logic based on location accuracy + // Address display logic based on location accuracy let addressDisplayHTML = ""; let addressToShow = record.checked_in_address; let addressIcon = "fas fa-location-arrow"; @@ -812,8 +810,8 @@ function createTableRow(record, displayIndex) {
- - + + ${ record.qr_address.length > 50 ? record.qr_address.substring(0, 50) + "..." @@ -1019,8 +1017,19 @@ function exportAttendanceWithAccuracy() { } function exportAttendance() { + // Check user role before proceeding + const userRole = window.userRole; // Read from global variable set in template + console.log("Template - session.role:", '{{ session.role }}'); + console.log("Template - window.userRole set to:", window.userRole); + + if (!['admin', 'payroll'].includes(userRole)) { + console.log("Export access denied - insufficient privileges"); + alert("Access denied. Only administrators and payroll staff can export data."); + return; + } + // Log export action - console.log("Export button clicked - redirecting to configuration page"); + console.log(`Export button clicked by ${userRole} - redirecting to configuration page`); // Get current filters const currentFilters = getCurrentFilters(); @@ -1055,6 +1064,15 @@ function getCurrentFilters() { // Add a quick CSV export function as backup (keep existing functionality) function exportAttendanceCSV() { + // Check user role before proceeding + const userRole = window.userRole; + + if (!['admin', 'payroll'].includes(userRole)) { + console.log("CSV export access denied - insufficient privileges"); + alert("Access denied. Only administrators and payroll staff can export data."); + return; + } + // Build export URL with current filters for CSV const params = new URLSearchParams(); const filters = getCurrentFilters(); diff --git a/static/js/export_configuration.js b/static/js/export_configuration.js index df38f6b..8c76040 100644 --- a/static/js/export_configuration.js +++ b/static/js/export_configuration.js @@ -535,6 +535,13 @@ function loadSavedPreferences() { const savedPrefs = localStorage.getItem('exportPreferences'); if (!savedPrefs) { console.log('No saved preferences found'); + // Check if there are already selected columns on page load and update preview + const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked'); + if (alreadySelected.length > 0) { + console.log('Found pre-selected columns, updating preview'); + updateSelectedColumnsList(); + updatePreview(); + } return; } @@ -547,6 +554,13 @@ function loadSavedPreferences() { if (savedDate < thirtyDaysAgo) { localStorage.removeItem('exportPreferences'); console.log('Saved preferences are too old, removed'); + // Check if there are already selected columns on page load and update preview + const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked'); + if (alreadySelected.length > 0) { + console.log('Found pre-selected columns, updating preview'); + updateSelectedColumnsList(); + updatePreview(); + } return; } @@ -585,6 +599,10 @@ function loadSavedPreferences() { console.log('Preferences loaded:', prefs); + // Update preview after loading preferences + updateSelectedColumnsList(); + updatePreview(); + } catch (e) { console.warn('Could not load saved preferences:', e); try { @@ -592,6 +610,14 @@ function loadSavedPreferences() { } catch (removeError) { console.warn('Could not remove invalid preferences:', removeError); } + + // Check if there are already selected columns on page load and update preview + const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked'); + if (alreadySelected.length > 0) { + console.log('Found pre-selected columns after preference error, updating preview'); + updateSelectedColumnsList(); + updatePreview(); + } } } diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index 435fc54..358c772 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -59,6 +59,7 @@ document.addEventListener("DOMContentLoaded", function () { // CRITICAL: Initialize systems in correct order initializeLanguage(); + initializeLocationServicesCheck(); initializeStaffIdPersistence(); initializeForm(); initializeLocation(); @@ -733,3 +734,152 @@ function startClock() { updateClock(); setInterval(updateClock, 1000); } + +function checkLocationServicesStatus() { + console.log("📱 Checking location services status..."); + + // Check if geolocation is supported + if (!navigator.geolocation) { + console.log("❌ Geolocation not supported by this browser"); + showLocationServicesWarning("not_supported"); + return; + } + + // Test location access with a quick check + const timeoutId = setTimeout(() => { + console.log("⏰ Location permission check timed out"); + showLocationServicesWarning("timeout"); + }, 3000); // 3 second timeout + + navigator.geolocation.getCurrentPosition( + (position) => { + // Success - location services are working + clearTimeout(timeoutId); + console.log("✅ Location services are available and enabled"); + hideLocationServicesWarning(); + }, + (error) => { + // Error - location services may be disabled + clearTimeout(timeoutId); + console.log("❌ Location services error:", error.message); + + switch (error.code) { + case error.PERMISSION_DENIED: + showLocationServicesWarning("permission_denied"); + break; + case error.POSITION_UNAVAILABLE: + showLocationServicesWarning("position_unavailable"); + break; + case error.TIMEOUT: + showLocationServicesWarning("timeout"); + break; + default: + showLocationServicesWarning("unknown_error"); + break; + } + }, + { + enableHighAccuracy: false, + timeout: 2500, + maximumAge: 60000 + } + ); +} + +/** + * Show location services warning banner + */ +function showLocationServicesWarning(errorType) { + // Remove existing warning if present + hideLocationServicesWarning(); + + const warningMessages = { + en: { + not_supported: "Location services are not supported by your browser.
Los servicios de ubicación no son compatibles con su navegador.", + permission_denied: "Location access has been denied. Please enable location services for accurate check-in.
Se ha denegado el acceso a la ubicación. Habilite los servicios de ubicación para un registro preciso.", + position_unavailable: "Location services appear to be disabled. Please turn on location services for accurate check-in.
Los servicios de ubicación parecen estar deshabilitados. Active los servicios de ubicación para un registro preciso.", + timeout: "Location services may be disabled. Please check your location settings for accurate check-in.
Los servicios de ubicación pueden estar deshabilitados. Verifique su configuración de ubicación para un registro preciso.", + unknown_error: "Unable to access location services. Please check your location settings.
No se puede acceder a los servicios de ubicación. Verifique su configuración de ubicación." + }, + es: { + not_supported: "Los servicios de ubicación no son compatibles con su navegador.", + permission_denied: "Se ha denegado el acceso a la ubicación. Habilite los servicios de ubicación para un registro preciso.", + position_unavailable: "Los servicios de ubicación parecen estar deshabilitados. Active los servicios de ubicación para un registro preciso.", + timeout: "Los servicios de ubicación pueden estar deshabilitados. Verifique su configuración de ubicación para un registro preciso.", + unknown_error: "No se puede acceder a los servicios de ubicación. Verifique su configuración de ubicación." + } + }; + + const currentLang = currentLanguage || 'en'; + const message = warningMessages[currentLang][errorType] || warningMessages['en'][errorType]; + + // Create warning banner + const warningBanner = document.createElement('div'); + warningBanner.id = 'locationServicesWarning'; + warningBanner.className = 'location-warning-banner'; + warningBanner.innerHTML = ` +
+ +
+ ${message} +
+ + +
+
+
+ `; + + // Insert warning at the top of the page + const container = document.querySelector('.destination-container'); + if (container) { + container.insertBefore(warningBanner, container.firstChild); + } + + // Log warning event + console.log(`⚠️ Location services warning displayed: ${errorType}`); +} + +/** + * Hide location services warning banner + */ +function hideLocationServicesWarning() { + const existingWarning = document.getElementById('locationServicesWarning'); + if (existingWarning) { + existingWarning.remove(); + console.log("✅ Location services warning hidden"); + } +} + +/** + * Modified DOMContentLoaded event handler + * Add this to your existing initialization + */ +function initializeLocationServicesCheck() { + // Check location services status when page loads + setTimeout(() => { + checkLocationServicesStatus(); + }, 1000); // Small delay to ensure page is fully loaded + + // Also check before form submission + const originalHandleFormSubmit = handleFormSubmit; + window.handleFormSubmit = function(event) { + // Quick location check before submission + checkLocationServicesStatus(); + + // Continue with original form submission after brief delay + setTimeout(() => { + originalHandleFormSubmit.call(this, event); + }, 500); + }; +} diff --git a/templates/attendance_report.html b/templates/attendance_report.html index aa907d7..53ebb4c 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -10,7 +10,7 @@ {% endblock %} {% block content %} -
+
@@ -22,10 +22,12 @@
+ {% if session.role in ['admin', 'payroll'] %} + {% endif %}
- -
- - {% if record.address_source == 'qr' %} - - - - {{ record.qr_address[:45] }}{% if record.qr_address|length > 45 %}...{% endif %} - - {% else %} - - - {% if record.location_accuracy and record.location_accuracy > 0.5 %} - - {% endif %} - {{ record.checked_in_address[:45] }}{% if record.checked_in_address|length > 45 %}...{% endif %} - - {% endif %} + +
+ + + {% if record.qr_address %} + {{ record.qr_address[:50] }}{{ '...' if record.qr_address|length > 50 else '' }} + {% else %} + N/A + {% endif %} +
@@ -634,5 +624,6 @@ document.addEventListener('DOMContentLoaded', function() { console.log('✅ Admin/Payroll edit/delete buttons have been activated'); } }); +window.userRole = '{{ session.role }}'; {% endblock %} \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index a56d18a..3733f67 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -173,11 +173,12 @@ .project-qr-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: var(--spacing-4); padding: var(--spacing-6); } +/* QR Card Base Styles */ .qr-card { background: var(--white); border: 1px solid var(--gray-200); @@ -199,14 +200,17 @@ background: linear-gradient(135deg, var(--white), #f8fafc); } -.qr-card-header { +/* Compact QR Card Layout for Unassigned Cards */ +.qr-card-compact { display: flex; - align-items: flex-start; + align-items: center; gap: var(--spacing-3); - margin-bottom: var(--spacing-3); + width: 100%; + padding: var(--spacing-3); + min-height: 80px; } -.qr-preview { +.qr-preview-compact { width: 60px; height: 60px; flex-shrink: 0; @@ -214,49 +218,140 @@ overflow: hidden; border: 2px solid var(--gray-200); transition: var(--transition); - cursor: pointer; } -.qr-preview img { +.qr-preview-compact img { width: 100%; height: 100%; object-fit: cover; } -.qr-card:hover .qr-preview { +.qr-card:hover .qr-preview-compact { border-color: var(--primary-color); } -.qr-info { +.qr-info-compact { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-1); +} + +.qr-name-compact { + font-size: var(--font-size-sm); + font-weight: 600; + color: var(--gray-900); + margin: 0; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.qr-details-row { + display: flex; + gap: var(--spacing-3); + flex-wrap: wrap; + align-items: center; +} + +.qr-url-short { + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.qr-status-compact { + flex-shrink: 0; +} + +.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-3); + margin-bottom: var(--spacing-3); + align-items: flex-start; +} + +.qr-preview-large { + width: 80px; + height: 80px; + 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-1); + min-width: 0; } .qr-name { - font-size: var(--font-size-base); + font-size: var(--font-size-sm); font-weight: 600; color: var(--gray-900); margin: 0 0 var(--spacing-1) 0; - line-height: 1.4; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.qr-location { - color: var(--gray-600); - font-size: var(--font-size-sm); +.qr-detail-item { display: flex; align-items: center; - gap: var(--spacing-1); - margin-bottom: var(--spacing-2); + gap: 0.4rem; + font-size: var(--font-size-xs); + color: var(--gray-600); + min-width: 0; +} + +.qr-detail-item i { + font-size: 0.65rem; + width: auto; + flex-shrink: 0; + color: var(--gray-400); +} + +.qr-detail-item span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } .qr-status-badge { display: inline-flex; align-items: center; gap: var(--spacing-1); - padding: 0.25rem 0.5rem; + padding: 0.2rem 0.4rem; border-radius: var(--radius-full); - font-size: var(--font-size-xs); + font-size: 0.65rem; font-weight: 500; + margin-top: var(--spacing-1); } .qr-status-badge.active { @@ -269,23 +364,18 @@ color: #dc2626; } -.qr-actions { - position: absolute; - top: var(--spacing-3); - right: var(--spacing-3); +.qr-card-actions { display: flex; gap: var(--spacing-1); - opacity: 0; - transition: var(--transition); -} - -.qr-card:hover .qr-actions { - opacity: 1; + justify-content: flex-start; + padding-top: var(--spacing-2); + border-top: 1px solid var(--gray-200); + margin-top: var(--spacing-2); } .qr-action-btn { - width: 32px; - height: 32px; + width: 28px; + height: 28px; border-radius: var(--radius-md); border: none; display: flex; @@ -293,7 +383,7 @@ justify-content: center; cursor: pointer; transition: var(--transition); - font-size: var(--font-size-sm); + font-size: 0.75rem; text-decoration: none; } @@ -317,16 +407,6 @@ 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 { background: rgba(245, 158, 11, 0.1); color: #d97706; @@ -337,6 +417,7 @@ color: var(--white); } +/* Empty States */ .empty-project-qr { text-align: center; padding: var(--spacing-8) var(--spacing-4); @@ -368,6 +449,41 @@ 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 { background: var(--white); border: 1px solid var(--gray-200); @@ -404,6 +520,7 @@ font-weight: 600; } +/* Modal Styles */ .modal { display: none; position: fixed; @@ -517,26 +634,26 @@ padding-bottom: var(--spacing-2); } -.qr-detail-item { +.qr-detail-section .qr-detail-item { display: grid; - grid-template-columns: 120px 1fr; + grid-template-columns: 100px 1fr; gap: var(--spacing-3); margin-bottom: var(--spacing-4); align-items: start; } -.qr-detail-item:last-child { +.qr-detail-section .qr-detail-item:last-child { margin-bottom: 0; } -.qr-detail-item label { +.qr-detail-section .qr-detail-item label { font-weight: 600; color: var(--gray-700); font-size: var(--font-size-sm); padding-top: 2px; } -.qr-detail-item span { +.qr-detail-section .qr-detail-item span { color: var(--gray-900); font-size: var(--font-size-base); line-height: 1.5; @@ -576,40 +693,106 @@ min-width: 140px; } -.dashboard-empty { - text-align: center; - padding: 4rem 2rem; - color: var(--gray-600); +/* Image Lightbox Styles */ +.image-lightbox { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; + cursor: pointer; } -.dashboard-empty .empty-icon { - width: 120px; - height: 120px; - margin: 0 auto 2rem; - background: var(--gray-100); +.lightbox-content { + position: relative; + max-width: 90vw; + max-height: 90vh; + text-align: center; +} + +.lightbox-image { + max-width: 100%; + max-height: 80vh; + border-radius: var(--radius-lg); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + background: var(--white); + padding: var(--spacing-4); +} + +.lightbox-close { + position: absolute; + top: -50px; + right: 0; + background: rgba(255, 255, 255, 0.9); + border: none; border-radius: 50%; + width: 40px; + height: 40px; + font-size: 1.2rem; + color: var(--gray-700); + cursor: pointer; + transition: var(--transition); display: flex; align-items: center; justify-content: center; - font-size: 3rem; - color: var(--gray-400); } -.dashboard-empty h3 { - font-size: 2rem; - font-weight: 600; +.lightbox-close:hover { + background: var(--white); 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; +.lightbox-info { + color: var(--white); + margin-top: var(--spacing-4); + font-size: var(--font-size-lg); + font-weight: 500; } +.lightbox-hint { + color: rgba(255, 255, 255, 0.7); + margin-top: var(--spacing-2); + font-size: var(--font-size-sm); +} + +/* Make QR images clickable */ +.qr-preview-large, +.qr-preview-compact { + cursor: zoom-in; + position: relative; +} + +.qr-preview-large::after, +.qr-preview-compact::after { + content: '🔍'; + position: absolute; + top: 2px; + right: 2px; + background: rgba(0, 0, 0, 0.7); + color: white; + border-radius: 50%; + width: 18px; + height: 18px; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: var(--transition); +} + +.qr-card:hover .qr-preview-large::after, +.qr-card:hover .qr-preview-compact::after { + opacity: 1; +} + +/* Animations */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } @@ -626,6 +809,7 @@ } } +/* Responsive Design */ @media (max-width: 768px) { .project-header { padding: var(--spacing-3) var(--spacing-4); @@ -646,19 +830,56 @@ align-items: flex-end; gap: var(--spacing-2); } - + .project-qr-grid { - grid-template-columns: 1fr; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-3); padding: var(--spacing-4); } - - .qr-card-header { + + .qr-card-compact { + flex-direction: column; + text-align: center; gap: var(--spacing-2); + padding: var(--spacing-2); } - .qr-preview { - width: 50px; - height: 50px; + .qr-info-compact { + width: 100%; + } + + .qr-details-row { + flex-direction: column; + gap: var(--spacing-1); + align-items: center; + } + + .qr-status-compact { + margin: 0; + } + + .qr-actions-compact { + justify-content: center; + } + + .qr-card-layout { + flex-direction: column; + text-align: center; + align-items: center; + } + + .qr-preview-large { + width: 70px; + height: 70px; + } + + .qr-details { + align-items: center; + width: 100%; + } + + .qr-detail-item { + justify-content: center; } .unassigned-header { @@ -691,11 +912,6 @@ height: 240px; } - .qr-detail-item { - grid-template-columns: 1fr; - gap: var(--spacing-1); - } - .qr-modal-footer { flex-direction: column; } @@ -712,12 +928,11 @@ overflow: visible; text-overflow: unset; } - - .qr-actions { - position: static; - opacity: 1; - margin-top: var(--spacing-2); - justify-content: flex-end; + + .project-qr-grid { + grid-template-columns: 1fr; + gap: var(--spacing-2); + padding: var(--spacing-3); } .modal-header { @@ -872,24 +1087,41 @@ data-qr-event="{{ qr.location_event }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" onclick="openQRModalFromData(this)"> -
-
+ +
+
QR Code for {{ qr.name }}
-
+ +

{{ qr.name }}

-

+

- {{ qr.location }} -

+ {{ qr.location }} +
+ + {% if qr.location_address %} +
+ + {{ qr.location_address }} +
+ {% endif %} + + {% if qr.qr_url %} +
+ + {{ qr.qr_url }} +
+ {% endif %} + {{ 'Active' if qr.active_status else 'Inactive' }}
- -
+ +
@@ -902,10 +1134,6 @@ - - {% endif %}
@@ -952,41 +1180,58 @@ data-qr-event="{{ qr.location_event }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" onclick="openQRModalFromData(this)"> -
-
+ +
+
QR Code for {{ qr.name }}
-
-

{{ qr.name }}

-

- - {{ qr.location }} -

+ +
+

{{ qr.name }}

+
+
+ + {{ qr.location }} +
+ + {% if qr.location_address %} +
+ + {{ qr.location_address }} +
+ {% endif %} + + {% if qr.qr_url %} +
+ + {{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif %} +
+ {% endif %} +
+
+ +
{{ 'Active' if qr.active_status else 'Inactive' }}
-
- -
- - - - - - {% if session.role == 'admin' %} - - - - {% endif %} +
+ + + + + + + {% if session.role == 'admin' %} + + {% endif %} +
{% endfor %} @@ -1018,6 +1263,18 @@ {% endif %}
+ +
+ +
+