From c6abd4521b1000d4fd47366274db0c8419cdb822 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 1 Aug 2025 12:50:36 -0400 Subject: [PATCH] Revert "Enhance attendance report" This reverts commit c07b815b78e774c513eb130637f4762c06df0d77. --- app.py | 170 +----- location_migration.py | 258 -------- static/css/attendance.css | 495 ++++------------ static/js/attendance_report.js | 898 ++++++++++++++++++---------- static/js/qr_destination.js | 970 +++++++++++++++++++------------ templates/attendance_report.html | 523 ++++------------- 6 files changed, 1425 insertions(+), 1889 deletions(-) delete mode 100644 location_migration.py diff --git a/app.py b/app.py index 2f92dd6..788c363 100644 --- a/app.py +++ b/app.py @@ -183,7 +183,6 @@ def generate_qr_code(data): return img_str - @app.template_filter('strftime') def strftime_filter(value, format='%Y-%m-%d'): """Format datetime/date/string as strftime""" @@ -627,68 +626,6 @@ def user_stats_api(): print(f"Error fetching user stats: {e}") return jsonify({'error': 'Failed to fetch user statistics'}), 500 -# Optional: Add a separate endpoint to get location data as JSON for AJAX requests -@app.route('/api/attendance-location-data') -@admin_required -def attendance_location_data(): - """API endpoint to get attendance data with location information""" - try: - # Get query parameters - limit = request.args.get('limit', 100, type=int) - offset = request.args.get('offset', 0, type=int) - - query_sql = """ - SELECT - ad.employee_id, - ad.check_in_date, - ad.check_in_time, - ad.latitude, - ad.longitude, - ad.location_accuracy, - ad.address, - ad.location_source, - ad.location_name, - qr.name as qr_name - FROM attendance_data ad - JOIN qr_codes qr ON ad.qr_code_id = qr.id - WHERE ad.latitude IS NOT NULL AND ad.longitude IS NOT NULL - ORDER BY ad.created_timestamp DESC - LIMIT :limit OFFSET :offset - """ - - query = db.session.execute(text(query_sql), { - 'limit': limit, - 'offset': offset - }) - - records = [] - for row in query.fetchall(): - records.append({ - 'employee_id': row.employee_id, - 'check_in_date': row.check_in_date.strftime('%Y-%m-%d') if row.check_in_date else '', - 'check_in_time': row.check_in_time.strftime('%H:%M') if row.check_in_time else '', - 'latitude': row.latitude, - 'longitude': row.longitude, - 'accuracy': row.location_accuracy, - 'address': row.address, - 'location_source': row.location_source, - 'location_name': row.location_name, - 'qr_name': row.qr_name - }) - - return jsonify({ - 'success': True, - 'records': records, - 'count': len(records) - }) - - except Exception as e: - print(f"Error getting location data: {e}") - return jsonify({ - 'success': False, - 'error': str(e) - }), 500 - @app.route('/users//permanently-delete', methods=['GET', 'POST']) @admin_required def permanently_delete_user(user_id): @@ -1431,97 +1368,40 @@ def toggle_qr_status_api(qr_id): return redirect(url_for('dashboard')) @app.route('/attendance') -@admin_required +#@admin_required def attendance_report(): - """Enhanced attendance report page with location data""" + """Attendance report page (Admin only)""" try: # Get filter parameters date_filter = request.args.get('date', '') location_filter = request.args.get('location', '') employee_filter = request.args.get('employee', '') - # Enhanced query to include location data - query_sql = """ - SELECT - ad.id, - ad.employee_id, - ad.check_in_date, - ad.check_in_time, - ad.device_info, - ad.user_agent, - ad.ip_address, - ad.location_name, - ad.status, - ad.created_timestamp, - -- Location data columns - ad.latitude, - ad.longitude, - ad.location_accuracy, - ad.address, - ad.location_source, - ad.location_timestamp, - -- QR code info - qr.name as qr_name, - qr.location as qr_location, - qr.location_event, - qr.location_address as qr_address - FROM attendance_data ad - JOIN qr_codes qr ON ad.qr_code_id = qr.id - WHERE 1=1 - """ + # Base query using the view + query = db.session.execute(text("SELECT * FROM attendance_report WHERE 1=1")) - # Apply filters to SQL query - filter_params = {} - - if date_filter: - query_sql += " AND ad.check_in_date = :date_filter" - filter_params['date_filter'] = date_filter - - if location_filter: - query_sql += " AND ad.location_name ILIKE :location_filter" - filter_params['location_filter'] = f"%{location_filter}%" - - if employee_filter: - query_sql += " AND ad.employee_id ILIKE :employee_filter" - filter_params['employee_filter'] = f"%{employee_filter}%" - - # Order by latest first - query_sql += " ORDER BY ad.created_timestamp DESC" - - # Execute query - query = db.session.execute(text(query_sql), filter_params) + # Apply filters (you can enhance this with proper SQLAlchemy filtering) attendance_records = query.fetchall() # Get unique locations for filter dropdown locations_query = db.session.execute(text(""" SELECT DISTINCT location_name FROM attendance_data - WHERE location_name IS NOT NULL ORDER BY location_name """)) locations = [row[0] for row in locations_query.fetchall()] - # Enhanced statistics including location data + # Get attendance statistics stats_query = db.session.execute(text(""" SELECT COUNT(*) as total_checkins, COUNT(DISTINCT employee_id) as unique_employees, COUNT(DISTINCT qr_code_id) as active_locations, - COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins, - -- Location statistics - COUNT(CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN 1 END) as checkins_with_location, - COUNT(CASE WHEN location_accuracy <= 50 THEN 1 END) as high_accuracy_checkins, - COUNT(CASE WHEN location_accuracy > 50 AND location_accuracy <= 100 THEN 1 END) as medium_accuracy_checkins, - COUNT(CASE WHEN location_accuracy > 100 THEN 1 END) as low_accuracy_checkins + COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins FROM attendance_data """)) stats = stats_query.fetchone() - # Calculate location coverage percentage - location_coverage = 0 - if stats.total_checkins > 0: - location_coverage = round((stats.checkins_with_location / stats.total_checkins) * 100, 1) - # Add today's date for template today_date = datetime.now().strftime('%Y-%m-%d') current_date_formatted = datetime.now().strftime('%B %d') @@ -1530,7 +1410,6 @@ def attendance_report(): attendance_records=attendance_records, locations=locations, stats=stats, - location_coverage=location_coverage, date_filter=date_filter, location_filter=location_filter, employee_filter=employee_filter, @@ -1539,8 +1418,6 @@ def attendance_report(): except Exception as e: print(f"Error loading attendance report: {e}") - import traceback - traceback.print_exc() flash('Error loading attendance report.', 'error') return redirect(url_for('dashboard')) @@ -1779,41 +1656,8 @@ def update_existing_qr_codes(): print(f"Error updating existing QR codes: {e}") db.session.rollback() -def ensure_location_columns(): - """Ensure location columns exist in attendance_data table""" - try: - # Check if location columns exist - result = db.session.execute(text(""" - SELECT column_name - FROM information_schema.columns - WHERE table_name = 'attendance_data' - AND column_name IN ('latitude', 'longitude', 'location_accuracy', - 'address', 'location_source', 'location_timestamp') - """)).fetchall() - - existing_columns = [row[0] for row in result] - required_columns = ['latitude', 'longitude', 'location_accuracy', 'address', 'location_source', 'location_timestamp'] - missing_columns = [col for col in required_columns if col not in existing_columns] - - if missing_columns: - print(f"⚠️ Missing location columns: {', '.join(missing_columns)}") - print(" Run the location migration script first!") - return False - else: - print(f"✅ All location columns exist") - return True - - except Exception as e: - print(f"⚠️ Could not check location columns: {e}") - return False - if __name__ == '__main__': with app.app_context(): create_tables() update_existing_qr_codes() - - # Check location columns - if not ensure_location_columns(): - print("\n🚨 Location columns missing! Run location migration first:") - print("python location_migration.py") app.run(debug=True, host="0.0.0.0") diff --git a/location_migration.py b/location_migration.py deleted file mode 100644 index 94284b5..0000000 --- a/location_migration.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python3 -""" -Location Database Migration -Adds location columns to attendance_data table -""" - -import psycopg2 -import os -import sys - -# Database connection -DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres1411@localhost/qr_management') - -def add_location_columns(): - """Add location tracking columns to attendance_data table""" - - print("🚀 Adding location columns to attendance_data table...") - - try: - # Connect to database - conn = psycopg2.connect(DATABASE_URL) - cursor = conn.cursor() - - print("✅ Connected to database") - - # Check if attendance_data table exists - cursor.execute(""" - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'attendance_data' - ); - """) - - if not cursor.fetchone()[0]: - print("❌ attendance_data table not found!") - print(" Make sure your QR system is running first") - return False - - print("✅ attendance_data table found") - - # Add location columns one by one with error handling - location_columns = [ - { - 'name': 'latitude', - 'sql': 'ALTER TABLE attendance_data ADD COLUMN latitude DOUBLE PRECISION', - 'description': 'GPS latitude coordinate' - }, - { - 'name': 'longitude', - 'sql': 'ALTER TABLE attendance_data ADD COLUMN longitude DOUBLE PRECISION', - 'description': 'GPS longitude coordinate' - }, - { - 'name': 'location_accuracy', - 'sql': 'ALTER TABLE attendance_data ADD COLUMN location_accuracy DOUBLE PRECISION', - 'description': 'GPS accuracy in meters' - }, - { - 'name': 'address', - 'sql': 'ALTER TABLE attendance_data ADD COLUMN address VARCHAR(500)', - 'description': 'Resolved address from coordinates' - }, - { - 'name': 'location_source', - 'sql': "ALTER TABLE attendance_data ADD COLUMN location_source VARCHAR(50) DEFAULT 'manual'", - 'description': 'Source of location data (gps, network, manual)' - }, - { - 'name': 'location_timestamp', - 'sql': 'ALTER TABLE attendance_data ADD COLUMN location_timestamp TIMESTAMP', - 'description': 'When location was captured' - } - ] - - added_columns = [] - skipped_columns = [] - - for column in location_columns: - try: - # Check if column already exists - cursor.execute(""" - SELECT EXISTS ( - SELECT FROM information_schema.columns - WHERE table_name = 'attendance_data' - AND column_name = %s - ); - """, (column['name'],)) - - if cursor.fetchone()[0]: - print(f" ⏭️ Column '{column['name']}' already exists") - skipped_columns.append(column['name']) - continue - - # Add the column - cursor.execute(column['sql']) - added_columns.append(column['name']) - print(f" ✅ Added '{column['name']}' - {column['description']}") - - except Exception as e: - print(f" ❌ Error adding '{column['name']}': {e}") - - # Commit all changes - conn.commit() - - print(f"\n📊 Migration Summary:") - print(f" ✅ Added columns: {len(added_columns)}") - print(f" ⏭️ Skipped columns: {len(skipped_columns)}") - - if added_columns: - print(f" 📝 New columns: {', '.join(added_columns)}") - - if skipped_columns: - print(f" 📝 Existing columns: {', '.join(skipped_columns)}") - - # Verify the columns were added - cursor.execute(""" - SELECT column_name, data_type - FROM information_schema.columns - WHERE table_name = 'attendance_data' - AND column_name IN ('latitude', 'longitude', 'location_accuracy', - 'address', 'location_source', 'location_timestamp') - ORDER BY column_name; - """) - - columns_info = cursor.fetchall() - - print(f"\n📋 Location columns in database:") - for col_name, col_type in columns_info: - print(f" • {col_name}: {col_type}") - - # Check existing data - cursor.execute("SELECT COUNT(*) FROM attendance_data") - total_records = cursor.fetchone()[0] - print(f"\n📊 Total attendance records: {total_records}") - - cursor.close() - conn.close() - - print("\n🎉 Location columns added successfully!") - print("\nNext step: Update your app.py to save location data") - - return True - - except psycopg2.Error as e: - print(f"❌ Database error: {e}") - return False - except Exception as e: - print(f"❌ Unexpected error: {e}") - return False - -def verify_columns(): - """Verify location columns exist and show sample data""" - try: - conn = psycopg2.connect(DATABASE_URL) - cursor = conn.cursor() - - # Check columns exist - cursor.execute(""" - SELECT column_name, data_type, is_nullable - FROM information_schema.columns - WHERE table_name = 'attendance_data' - AND column_name IN ('latitude', 'longitude', 'location_accuracy', - 'address', 'location_source', 'location_timestamp') - ORDER BY column_name; - """) - - columns = cursor.fetchall() - - print(f"\n🔍 Verification Results:") - print(f" Found {len(columns)} location columns:") - - for col_name, col_type, nullable in columns: - null_text = "NULL" if nullable == "YES" else "NOT NULL" - print(f" • {col_name}: {col_type} ({null_text})") - - # Show sample with location data if any exists - cursor.execute(""" - SELECT employee_id, latitude, longitude, location_accuracy, - address, location_source, created_timestamp - FROM attendance_data - WHERE latitude IS NOT NULL - ORDER BY created_timestamp DESC - LIMIT 3; - """) - - location_records = cursor.fetchall() - - if location_records: - print(f"\n📍 Sample records with location data:") - for record in location_records: - emp_id, lat, lng, acc, addr, source, timestamp = record - print(f" • {emp_id}: {lat:.6f}, {lng:.6f} (±{acc}m) - {source}") - if addr: - print(f" Address: {addr}") - else: - print(f"\n📍 No location data found yet (will be captured on next check-ins)") - - cursor.close() - conn.close() - - return True - - except Exception as e: - print(f"❌ Verification failed: {e}") - return False - -def test_connection(): - """Test database connection""" - try: - conn = psycopg2.connect(DATABASE_URL) - cursor = conn.cursor() - cursor.execute("SELECT version();") - version = cursor.fetchone()[0] - print(f"✅ Database connection successful") - print(f" PostgreSQL version: {version.split()[1]}") - cursor.close() - conn.close() - return True - except Exception as e: - print(f"❌ Database connection failed: {e}") - return False - -if __name__ == "__main__": - print("📍 QR Code System - Location Database Migration") - print("=" * 60) - - # Test connection first - if not test_connection(): - print("\n❌ Cannot connect to database. Please check:") - print("1. PostgreSQL is running") - print("2. Database credentials are correct") - print("3. Database 'qr_management' exists") - sys.exit(1) - - # Run migration - success = add_location_columns() - - if success: - # Verify columns were added - verify_columns() - - print("\n✅ Database migration completed!") - print("\n📋 Next steps:") - print("1. Update your app.py with the location-saving code") - print("2. Restart your Flask application") - print("3. Test check-in with location data") - print("4. Check database for saved location records") - else: - print("\n❌ Migration failed!") - print("\nManual SQL commands (if needed):") - print("ALTER TABLE attendance_data ADD COLUMN latitude DOUBLE PRECISION;") - print("ALTER TABLE attendance_data ADD COLUMN longitude DOUBLE PRECISION;") - print("ALTER TABLE attendance_data ADD COLUMN location_accuracy DOUBLE PRECISION;") - print("ALTER TABLE attendance_data ADD COLUMN address VARCHAR(500);") - print("ALTER TABLE attendance_data ADD COLUMN location_source VARCHAR(50) DEFAULT 'manual';") - print("ALTER TABLE attendance_data ADD COLUMN location_timestamp TIMESTAMP;") - - print("\n" + "=" * 60) \ No newline at end of file diff --git a/static/css/attendance.css b/static/css/attendance.css index f3174eb..db4447d 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -1,3 +1,8 @@ +/** + * Attendance Report Page Styles + * static/css/attendance.css + */ + /* Page Container */ .attendance-page { max-width: 1400px; @@ -104,43 +109,7 @@ } .stat-card.warning::before { - background: linear-gradient(90deg, var(--warning-color), #d97706); -} - -.stat-card.danger::before { - background: linear-gradient(90deg, var(--danger-color), #b91c1c); -} - -/* NEW: Enhanced Statistics Card for Location Data */ -.stat-card.location-stats { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border: none; -} - -.stat-card.location-stats::before { - background: linear-gradient(90deg, #667eea, #764ba2); -} - -.stat-card.location-stats .stat-icon { - background: rgba(255, 255, 255, 0.2); - color: white; - border: 2px solid rgba(255, 255, 255, 0.3); -} - -.stat-card.location-stats .stat-content h3 { - color: white; -} - -.stat-card.location-stats .stat-content p { - color: rgba(255, 255, 255, 0.9); -} - -.stat-card.location-stats .stat-content small { - color: rgba(255, 255, 255, 0.8); - font-size: 0.75rem; - display: block; - margin-top: 4px; + background: linear-gradient(90deg, var(--warning-color), #b45309); } .stat-card:hover { @@ -160,24 +129,20 @@ flex-shrink: 0; } -.stat-icon.primary { +.stat-card.primary .stat-icon { background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); } -.stat-icon.success { +.stat-card.success .stat-icon { background: linear-gradient(135deg, var(--success-color), #047857); } -.stat-icon.info { +.stat-card.info .stat-icon { background: linear-gradient(135deg, var(--info-color), #0369a1); } -.stat-icon.warning { - background: linear-gradient(135deg, var(--warning-color), #d97706); -} - -.stat-icon.danger { - background: linear-gradient(135deg, var(--danger-color), #b91c1c); +.stat-card.warning .stat-icon { + background: linear-gradient(135deg, var(--warning-color), #b45309); } .stat-content { @@ -189,13 +154,20 @@ font-weight: 700; color: var(--gray-900); margin-bottom: var(--spacing-1); + line-height: 1; } .stat-content p { + color: var(--gray-600); + font-size: var(--font-size-base); + margin-bottom: var(--spacing-1); + font-weight: 500; +} + +.stat-trend { font-size: var(--font-size-sm); color: var(--gray-500); - margin: 0; - font-weight: 500; + font-style: italic; } /* Filters Section */ @@ -211,7 +183,7 @@ } .filters-header { - padding: var(--spacing-6) var(--spacing-6) var(--spacing-4); + padding: var(--spacing-6); border-bottom: 1px solid var(--gray-200); background: var(--gray-50); } @@ -230,11 +202,14 @@ color: var(--primary-color); } +.filters-form { + padding: var(--spacing-6); +} + .filter-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--spacing-4); - padding: var(--spacing-6); align-items: end; } @@ -245,9 +220,9 @@ } .filter-group label { + font-size: var(--font-size-sm); font-weight: 600; color: var(--gray-700); - font-size: var(--font-size-sm); display: flex; align-items: center; gap: var(--spacing-2); @@ -315,12 +290,6 @@ color: var(--primary-color); } -.record-count { - font-size: 14px; - color: #6c757d; - font-weight: normal; -} - .table-controls { display: flex; align-items: center; @@ -385,46 +354,12 @@ font-size: var(--font-size-xs); } -/* NEW: Enhanced Table Styles for Location Columns */ -.attendance-table th:nth-child(7), -.attendance-table th:nth-child(8), -.attendance-table th:nth-child(9), -.attendance-table th:nth-child(10) { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - border-left: 2px solid #667eea; - position: relative; -} - -.attendance-table th:nth-child(7)::before { - content: "📍"; - position: absolute; - left: 4px; - top: 50%; - transform: translateY(-50%); - font-size: 10px; -} - -.attendance-table th[onclick*="sortTable(6)"] i, -.attendance-table th[onclick*="sortTable(7)"] i, -.attendance-table th[onclick*="sortTable(8)"] i, -.attendance-table th[onclick*="sortTable(9)"] i { - color: #667eea; -} - .attendance-table td { padding: var(--spacing-4) var(--spacing-3); border-bottom: 1px solid var(--gray-200); vertical-align: middle; } -.attendance-table td:nth-child(7), -.attendance-table td:nth-child(8), -.attendance-table td:nth-child(9), -.attendance-table td:nth-child(10) { - background: rgba(102, 126, 234, 0.02); - border-left: 1px solid #e5e7eb; -} - .attendance-table tbody tr { transition: var(--transition); } @@ -433,20 +368,6 @@ background: var(--gray-50); } -.attendance-table tbody tr:hover .location-data, -.attendance-table tbody tr:hover .accuracy-badge, -.attendance-table tbody tr:hover .location-indicator { - transform: scale(1.02); - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} - -.attendance-table tbody tr:focus, -.attendance-table tbody tr:focus-within { - background: rgba(102, 126, 234, 0.1) !important; - outline: 2px solid #667eea; - outline-offset: -2px; -} - /* Table Cell Content */ .employee-info { display: flex; @@ -509,129 +430,6 @@ color: var(--info-color); } -/* NEW: Location Data Specific Styles */ -.location-data { - font-family: 'Courier New', monospace; - font-size: 11px; - color: #495057; - background: #f8f9fa; - padding: 4px 6px; - border-radius: 4px; - margin: 2px 0; - border: 1px solid #e9ecef; -} - -.coordinates { - display: block; - line-height: 1.3; - white-space: nowrap; -} - -.accuracy-badge { - display: inline-block; - padding: 2px 6px; - border-radius: 10px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - margin-top: 2px; - border: 1px solid transparent; -} - -.accuracy-high { - background: #d4f4dd; - color: #166534; - border-color: #166534; -} - -.accuracy-medium { - background: #fef3c7; - color: #92400e; - border-color: #92400e; -} - -.accuracy-low { - background: #fee2e2; - color: #991b1b; - border-color: #991b1b; -} - -.location-indicator { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 8px; - border-radius: 12px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - border: 1px solid transparent; -} - -.has-gps { - background: #d4f4dd; - color: #166534; - border-color: #166534; -} - -.has-gps i { - color: #10b981; -} - -.no-gps { - background: #fee2e2; - color: #991b1b; - border-color: #991b1b; -} - -.no-gps i { - color: #ef4444; -} - -.address-info { - font-size: 11px; - color: #6c757d; - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-top: 2px; - padding: 2px 4px; - background: #f8f9fa; - border-radius: 3px; -} - -.address-info:hover { - white-space: normal; - max-width: none; - position: relative; - z-index: 10; - background: #fff; - box-shadow: 0 2px 8px rgba(0,0,0,0.15); - border: 1px solid #dee2e6; - padding: 6px 8px; -} - -.map-link { - color: #2563eb; - text-decoration: none; - font-size: 10px; - display: inline-flex; - align-items: center; - gap: 3px; - margin-top: 3px; - padding: 2px 4px; - border-radius: 3px; - transition: all 0.2s ease; -} - -.map-link:hover { - text-decoration: underline; - background: #eff6ff; - color: #1d4ed8; -} - /* Status Badge */ .status-badge { display: inline-flex; @@ -664,36 +462,20 @@ /* Action Buttons */ .record-actions { display: flex; - gap: 4px; - align-items: center; - justify-content: flex-start; + gap: var(--spacing-1); } .action-btn { - display: inline-flex; + width: 32px; + height: 32px; + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: var(--transition); + display: flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; - border: none; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - font-size: 10px; -} - -.action-btn:hover { - transform: scale(1.1); -} - -.action-btn[title*="View"] { - background: #eff6ff; - color: #2563eb; -} - -.action-btn[title*="View"]:hover { - background: #dbeafe; - color: #1d4ed8; + font-size: var(--font-size-xs); } .btn-view { @@ -729,28 +511,28 @@ /* Empty State */ .empty-state { text-align: center; - padding: 60px 20px; - color: #6b7280; + padding: var(--spacing-20); + color: var(--gray-500); } -.empty-state i { - font-size: 4em; - margin-bottom: 20px; - color: #d1d5db; +.empty-icon { + font-size: 4rem; + color: var(--gray-300); + margin-bottom: var(--spacing-4); } .empty-state h3 { - font-size: 1.5em; - margin-bottom: 10px; - color: #374151; + font-size: var(--font-size-xl); + color: var(--gray-700); + margin-bottom: var(--spacing-2); } .empty-state p { - margin-bottom: 20px; + font-size: var(--font-size-base); + margin-bottom: var(--spacing-6); max-width: 400px; margin-left: auto; margin-right: auto; - line-height: 1.5; } /* Pagination */ @@ -852,63 +634,65 @@ /* Modal Styles */ .modal { + display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; + z-index: var(--z-modal); + backdrop-filter: blur(4px); } .modal-content { - background: white; - border-radius: 12px; - max-width: 600px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--white); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-xl); width: 90%; + max-width: 600px; max-height: 80vh; overflow: hidden; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } .modal-header { - padding: 20px; - border-bottom: 1px solid #e5e7eb; - background: #f9fafb; + padding: var(--spacing-6); + border-bottom: 1px solid var(--gray-200); display: flex; justify-content: space-between; align-items: center; + background: var(--gray-50); } .modal-header h3 { margin: 0; - color: #111827; - display: flex; - align-items: center; - gap: 8px; + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--gray-900); } .modal-close { background: none; border: none; - font-size: 18px; + font-size: var(--font-size-xl); cursor: pointer; - color: #6b7280; - padding: 4px; - border-radius: 4px; - transition: all 0.2s ease; + color: var(--gray-500); + padding: var(--spacing-2); + border-radius: var(--radius); + transition: var(--transition); } .modal-close:hover { - background: #e5e7eb; - color: #374151; + background: var(--gray-200); + color: var(--gray-700); } .modal-body { - padding: 20px; + padding: var(--spacing-6); max-height: 60vh; overflow-y: auto; } @@ -922,81 +706,7 @@ background: var(--gray-50); } -.record-detail-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - font-size: 14px; -} - -.detail-item { - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px; - background: #f9fafb; - border-radius: 8px; - border: 1px solid #e5e7eb; -} - -.detail-item strong { - color: #374151; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 600; -} - -.detail-item span { - color: #111827; - font-weight: 500; - font-family: 'Courier New', monospace; - font-size: 13px; -} - -/* Loading States */ -.loading-location { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; -} - -@keyframes loading { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -/* Tooltip Enhancements */ -[title] { - cursor: help; -} - /* Responsive Design */ -@media (max-width: 1200px) { - .attendance-table { - font-size: 11px; - } - - .location-data { - font-size: 9px; - padding: 2px 4px; - } - - .accuracy-badge { - font-size: 8px; - padding: 1px 4px; - } - - .address-info { - max-width: 100px; - font-size: 9px; - } -} - @media (max-width: 768px) { .attendance-page { padding: var(--spacing-4); @@ -1055,20 +765,57 @@ .record-actions { flex-direction: column; } +} - /* Hide coordinates and address columns on mobile */ - .attendance-table th:nth-child(8), - .attendance-table td:nth-child(8), - .attendance-table th:nth-child(10), - .attendance-table td:nth-child(10) { +@media (max-width: 480px) { + .attendance-table th, + .attendance-table td { + padding: var(--spacing-2); + font-size: var(--font-size-xs); + } + + .employee-id { + font-size: var(--font-size-xs); + } + + .time-info { + font-size: var(--font-size-xs); + } + + .stat-icon { + width: 50px; + height: 50px; + font-size: 1.25rem; + } + + .stat-content h3 { + font-size: var(--font-size-2xl); + } +} + +/* Print Styles */ +@media print { + .attendance-page { + background: white; + box-shadow: none; + } + + .attendance-header, + .filters-section, + .charts-section { display: none; } - - .location-data { - font-size: 8px; - padding: 1px 3px; + + .attendance-table-section { + box-shadow: none; + border: 1px solid #ccc; } - - .coordinates { - display: none; /* Hide coordinates on mobile */ - } \ No newline at end of file + + .action-btn { + display: none; + } + + .pagination-container { + display: none; + } +} \ No newline at end of file diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 0a23fb3..6f7ccf3 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -1,6 +1,6 @@ /** - * Enhanced Attendance Report JavaScript with Location Support - * Fixed version - removes undefined function calls + * Attendance Report JavaScript + * Handles filtering, sorting, pagination, and analytics for attendance data */ // Global variables @@ -11,163 +11,144 @@ let sortDirection = 'asc'; let attendanceData = []; let filteredData = []; -// Charts (if you want to add them later) +// Charts let dailyChart = null; let locationChart = null; // Initialize page when DOM is loaded document.addEventListener('DOMContentLoaded', function() { - console.log('Enhanced Attendance Report page initialized'); + console.log('Attendance Report page initialized'); initializeReport(); + loadAttendanceData(); + initializeCharts(); setupEventListeners(); - - // Initialize location-specific features - initializeLocationFeatures(); }); function initializeReport() { - console.log('📊 Initializing attendance report...'); - - // Load data from existing table + // Load data from table loadTableData(); - // Initialize pagination if needed + // Initialize pagination updatePagination(); - // Apply any initial filters + // Apply initial filters if any applyFilters(); - - console.log('✅ Report initialized successfully'); } function loadTableData() { const table = document.getElementById('attendanceTable'); - if (!table) { - console.log('⚠️ Attendance table not found'); - return; + if (table) { + const rows = table.querySelectorAll('tbody tr'); + attendanceData = Array.from(rows).map((row, index) => { + const cells = row.querySelectorAll('td'); + return { + id: row.dataset.recordId, + index: index + 1, + employeeId: cells[1] ? cells[1].textContent.trim() : '', + location: cells[2] ? cells[2].textContent.trim() : '', + event: cells[3] ? cells[3].textContent.trim() : '', + date: cells[4] ? cells[4].textContent.trim() : '', + time: cells[5] ? cells[5].textContent.trim() : '', + device: cells[6] ? cells[6].getAttribute('title') || cells[6].textContent.trim() : '', + status: cells[7] ? cells[7].textContent.trim() : '', + element: row + }; + }); + + filteredData = [...attendanceData]; } - - const rows = table.querySelectorAll('tbody tr'); - attendanceData = Array.from(rows).map((row, index) => { - const cells = row.querySelectorAll('td'); - return { - id: row.dataset.recordId || index, - index: index + 1, - employeeId: cells[1] ? cells[1].textContent.trim() : '', - location: cells[2] ? cells[2].textContent.trim() : '', - event: cells[3] ? cells[3].textContent.trim() : '', - date: cells[4] ? cells[4].textContent.trim() : '', - time: cells[5] ? cells[5].textContent.trim() : '', - // Location data (new columns) - gpsStatus: cells[6] ? cells[6].textContent.trim() : '', - coordinates: cells[7] ? cells[7].textContent.trim() : '', - accuracy: cells[8] ? cells[8].textContent.trim() : '', - address: cells[9] ? cells[9].textContent.trim() : '', - device: cells[10] ? cells[10].getAttribute('title') || cells[10].textContent.trim() : '', - status: cells[11] ? cells[11].textContent.trim() : '', - row: row - }; - }); - - filteredData = [...attendanceData]; - console.log(`📋 Loaded ${attendanceData.length} attendance records`); } function setupEventListeners() { - // Entries per page selector + // Entries per page change const entriesSelect = document.getElementById('entriesPerPage'); if (entriesSelect) { entriesSelect.addEventListener('change', changeEntriesPerPage); } - // Search functionality (if you have a search input) - const searchInput = document.getElementById('searchInput'); - if (searchInput) { - searchInput.addEventListener('input', handleSearch); - } - - // Filter form submission - const filterForm = document.querySelector('form[action*="attendance"]'); - if (filterForm) { - filterForm.addEventListener('submit', handleFilterSubmit); - } - - console.log('👂 Event listeners set up'); -} - -function initializeLocationFeatures() { - console.log('📍 Initializing location features...'); - - // Count records with location data - const recordsWithLocation = attendanceData.filter(record => - record.gpsStatus && record.gpsStatus.includes('GPS') - ).length; - - const locationCoverage = attendanceData.length > 0 - ? Math.round((recordsWithLocation / attendanceData.length) * 100) - : 0; - - console.log(`📊 Location coverage: ${recordsWithLocation}/${attendanceData.length} (${locationCoverage}%)`); - - // Add click handlers for map links - document.querySelectorAll('.map-link').forEach(link => { - link.addEventListener('click', function(e) { - console.log('🗺️ Opening map link:', this.href); - }); - }); - - // Add click handlers for view record buttons - document.querySelectorAll('.action-btn[title*="View"]').forEach(btn => { - btn.addEventListener('click', function(e) { + // Filter form + const filtersForm = document.getElementById('filtersForm'); + if (filtersForm) { + filtersForm.addEventListener('submit', function(e) { e.preventDefault(); - const recordId = this.closest('tr').dataset.recordId; - if (recordId) { - viewRecord(recordId); - } + applyFilters(); }); - }); + } + + // Real-time employee filter + const employeeFilter = document.getElementById('employee'); + if (employeeFilter) { + employeeFilter.addEventListener('input', debounce(applyFilters, 300)); + } + + // Date and location filters + const dateFilter = document.getElementById('date'); + const locationFilter = document.getElementById('location'); + + if (dateFilter) { + dateFilter.addEventListener('change', applyFilters); + } + + if (locationFilter) { + locationFilter.addEventListener('change', applyFilters); + } } function changeEntriesPerPage() { const select = document.getElementById('entriesPerPage'); - if (!select) return; - entriesPerPage = select.value === 'all' ? filteredData.length : parseInt(select.value); currentPage = 1; - - console.log(`📄 Changed entries per page to: ${entriesPerPage}`); - displayPage(); + updateTable(); updatePagination(); } -function handleSearch(event) { - const searchTerm = event.target.value.toLowerCase().trim(); +function applyFilters() { + const dateFilter = document.getElementById('date')?.value || ''; + const locationFilter = document.getElementById('location')?.value || ''; + const employeeFilter = document.getElementById('employee')?.value.toLowerCase() || ''; - if (searchTerm === '') { - filteredData = [...attendanceData]; - } else { - filteredData = attendanceData.filter(record => - record.employeeId.toLowerCase().includes(searchTerm) || - record.location.toLowerCase().includes(searchTerm) || - record.event.toLowerCase().includes(searchTerm) || - record.address.toLowerCase().includes(searchTerm) - ); - } + filteredData = attendanceData.filter(record => { + const matchesDate = !dateFilter || record.date === dateFilter; + const matchesLocation = !locationFilter || record.location === locationFilter; + const matchesEmployee = !employeeFilter || + record.employeeId.toLowerCase().includes(employeeFilter); + + return matchesDate && matchesLocation && matchesEmployee; + }); currentPage = 1; - displayPage(); + updateTable(); updatePagination(); - - console.log(`🔍 Search results: ${filteredData.length} records found`); + updateFilterStats(); } -function handleFilterSubmit(event) { - // Let the form submit normally to reload with filters - console.log('🔽 Applying filters...'); +function clearFilters() { + // Clear form inputs + const form = document.getElementById('filtersForm'); + if (form) { + form.reset(); + } + + // Reset filtered data + filteredData = [...attendanceData]; + currentPage = 1; + + // Update display + updateTable(); + updatePagination(); + updateFilterStats(); + + // Update URL without filters + const url = new URL(window.location); + url.search = ''; + window.history.pushState({}, '', url); } function sortTable(columnIndex) { + const headers = ['index', 'employeeId', 'location', 'event', 'date', 'time', 'device', 'status']; + const column = headers[columnIndex]; + if (sortColumn === columnIndex) { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; } else { @@ -175,251 +156,568 @@ function sortTable(columnIndex) { sortDirection = 'asc'; } - // Update sort indicators - updateSortIndicators(); - - // Sort the data - const sortKey = getSortKey(columnIndex); - if (sortKey) { - filteredData.sort((a, b) => { - let aVal = a[sortKey] || ''; - let bVal = b[sortKey] || ''; - - // Convert to strings for comparison + filteredData.sort((a, b) => { + let aVal = a[column]; + let bVal = b[column]; + + // Handle different data types + if (column === 'date' || column === 'time') { + aVal = new Date(column === 'date' ? aVal : `2000-01-01 ${aVal}`); + bVal = new Date(column === 'date' ? bVal : `2000-01-01 ${bVal}`); + } else if (column === 'index') { + aVal = parseInt(aVal); + bVal = parseInt(bVal); + } else { aVal = aVal.toString().toLowerCase(); bVal = bVal.toString().toLowerCase(); - - if (sortDirection === 'asc') { - return aVal.localeCompare(bVal); - } else { - return bVal.localeCompare(aVal); - } - }); + } - displayPage(); - - console.log(`🔄 Sorted by column ${columnIndex} (${sortDirection})`); - } -} - -function getSortKey(columnIndex) { - const sortKeys = { - 0: 'index', - 1: 'employeeId', - 2: 'location', - 3: 'event', - 4: 'date', - 5: 'time', - 6: 'gpsStatus', - 7: 'coordinates', - 8: 'accuracy', - 9: 'address', - 10: 'device', - 11: 'status' - }; + if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; + if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; + return 0; + }); - return sortKeys[columnIndex]; + updateTable(); + updateSortIndicators(columnIndex); } -function updateSortIndicators() { - // Update sort arrows in table headers - document.querySelectorAll('.attendance-table th i.fas').forEach((icon, index) => { - icon.className = 'fas fa-sort'; - - if (index === sortColumn) { - icon.className = sortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'; +function updateSortIndicators(activeColumn) { + const headers = document.querySelectorAll('th[onclick]'); + headers.forEach((header, index) => { + const icon = header.querySelector('i'); + if (icon) { + if (index === activeColumn) { + icon.className = `fas fa-sort-${sortDirection === 'asc' ? 'up' : 'down'}`; + } else { + icon.className = 'fas fa-sort'; + } } }); } -function displayPage() { - const table = document.getElementById('attendanceTable'); - if (!table) return; - - const tbody = table.querySelector('tbody'); +function updateTable() { + const tbody = document.querySelector('#attendanceTable tbody'); if (!tbody) return; - // Hide all rows first - tbody.querySelectorAll('tr').forEach(row => { - row.style.display = 'none'; - }); - // Calculate pagination const startIndex = (currentPage - 1) * entriesPerPage; - const endIndex = startIndex + (entriesPerPage === filteredData.length ? filteredData.length : entriesPerPage); + const endIndex = entriesPerPage === filteredData.length ? + filteredData.length : + Math.min(startIndex + entriesPerPage, filteredData.length); - // Show relevant rows - for (let i = startIndex; i < endIndex && i < filteredData.length; i++) { - const record = filteredData[i]; - if (record.row) { - record.row.style.display = ''; + // Hide all rows first + attendanceData.forEach(record => { + if (record.element) { + record.element.style.display = 'none'; } - } + }); - console.log(`📄 Displaying records ${startIndex + 1}-${Math.min(endIndex, filteredData.length)} of ${filteredData.length}`); + // Show filtered and paginated rows + const visibleData = filteredData.slice(startIndex, endIndex); + visibleData.forEach((record, index) => { + if (record.element) { + record.element.style.display = ''; + // Update row number + const firstCell = record.element.querySelector('td:first-child'); + if (firstCell) { + firstCell.textContent = startIndex + index + 1; + } + } + }); + + // Show empty state if no data + showEmptyStateIfNeeded(); +} + +function showEmptyStateIfNeeded() { + const tbody = document.querySelector('#attendanceTable tbody'); + let emptyRow = tbody.querySelector('.empty-row'); + + if (filteredData.length === 0) { + if (!emptyRow) { + emptyRow = document.createElement('tr'); + emptyRow.className = 'empty-row'; + emptyRow.innerHTML = ` + +
+ +

No Records Found

+

No attendance records match your current filters.

+ +
+ + `; + tbody.appendChild(emptyRow); + } + emptyRow.style.display = ''; + } else if (emptyRow) { + emptyRow.style.display = 'none'; + } } function updatePagination() { - // This is a placeholder for pagination controls - // You can implement pagination UI here if needed + const container = document.getElementById('paginationContainer'); + if (!container) return; const totalPages = Math.ceil(filteredData.length / entriesPerPage); - console.log(`📄 Page ${currentPage} of ${totalPages}`); -} - -function applyFilters() { - // Filters are handled by the backend through form submission - // This function can be used for client-side filtering if needed - console.log('🔽 Filters applied'); -} - -// Export and utility functions -function exportToCSV() { - const table = document.getElementById('attendanceTable'); - if (!table) { - console.error('❌ Table not found for export'); + + if (totalPages <= 1) { + container.innerHTML = ''; return; } - let csv = []; - const rows = table.querySelectorAll('tr'); + let paginationHTML = ''; + + // Add pagination info + const startRecord = (currentPage - 1) * entriesPerPage + 1; + const endRecord = Math.min(currentPage * entriesPerPage, filteredData.length); + + paginationHTML += ` +
+ Showing ${startRecord} to ${endRecord} of ${filteredData.length} entries + ${filteredData.length !== attendanceData.length ? + `(filtered from ${attendanceData.length} total entries)` : ''} +
+ `; + + container.innerHTML = paginationHTML; } -function printReport() { - window.print(); - console.log('🖨️ Print dialog opened'); +function goToPage(page) { + const totalPages = Math.ceil(filteredData.length / entriesPerPage); + + if (page < 1 || page > totalPages) return; + + currentPage = page; + updateTable(); + updatePagination(); + + // Scroll to top of table + const table = document.getElementById('attendanceTable'); + if (table) { + table.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } } -function viewRecord(recordId) { - console.log('👁️ Viewing record:', recordId); +function updateFilterStats() { + // Update stats display if needed + const totalRecords = filteredData.length; + console.log(`Filtered records: ${totalRecords}`); +} + +// Record actions +function viewRecordDetails(recordId) { + const record = attendanceData.find(r => r.id == recordId); + if (!record) return; const modal = document.getElementById('recordModal'); - const detailsDiv = document.getElementById('recordDetails'); + const modalTitle = document.getElementById('modalTitle'); + const modalBody = document.getElementById('modalBody'); - if (!modal || !detailsDiv) { - console.error('❌ Modal elements not found'); - return; - } + if (!modal || !modalTitle || !modalBody) return; - // Find the record row - const row = document.querySelector(`tr[data-record-id="${recordId}"]`); - if (!row) { - console.error('❌ Record row not found'); - return; - } + modalTitle.textContent = `Attendance Record - ${record.employeeId}`; - const cells = row.querySelectorAll('td'); - - detailsDiv.innerHTML = ` -
-
- Employee ID: - ${cells[1] ? cells[1].textContent.trim() : 'N/A'} -
-
- Location: - ${cells[2] ? cells[2].textContent.trim() : 'N/A'} -
-
- Event: - ${cells[3] ? cells[3].textContent.trim() : 'N/A'} -
-
- Date: - ${cells[4] ? cells[4].textContent.trim() : 'N/A'} -
-
- Time: - ${cells[5] ? cells[5].textContent.trim() : 'N/A'} -
-
- GPS Status: - ${cells[6] ? cells[6].textContent.trim() : 'N/A'} -
-
- Coordinates: - ${cells[7] ? cells[7].textContent.trim() : 'N/A'} -
-
- Accuracy: - ${cells[8] ? cells[8].textContent.trim() : 'N/A'} -
-
- Address: - ${cells[9] ? cells[9].textContent.trim() : 'N/A'} -
-
- Device: - ${cells[10] ? cells[10].textContent.trim() : 'N/A'} -
-
- Status: - ${cells[11] ? cells[11].textContent.trim() : 'N/A'} + modalBody.innerHTML = ` +
+
+
+ Employee ID: + ${record.employeeId} +
+
+ Location: + ${record.location} +
+
+ Event: + ${record.event} +
+
+ Date: + ${record.date} +
+
+ Time: + ${record.time} +
+
+ Device: + ${record.device} +
+
+ Status: + + ${record.status} + +
`; modal.style.display = 'flex'; - console.log('✅ Record modal opened'); + setTimeout(() => modal.classList.add('show'), 10); } -function closeModal() { +function editRecord(recordId) { + // Placeholder for edit functionality + alert(`Edit functionality for record ${recordId} would be implemented here.`); +} + +function deleteRecord(recordId) { + const record = attendanceData.find(r => r.id == recordId); + if (!record) return; + + const confirmed = confirm( + `Are you sure you want to delete the attendance record for ${record.employeeId}?\n\n` + + `Date: ${record.date}\n` + + `Time: ${record.time}\n` + + `Location: ${record.location}\n\n` + + 'This action cannot be undone.' + ); + + if (confirmed) { + // Here you would make an API call to delete the record + console.log(`Deleting record ${recordId}`); + + // For demo purposes, just remove from current data + const index = attendanceData.findIndex(r => r.id == recordId); + if (index > -1) { + // Remove from DOM + if (attendanceData[index].element) { + attendanceData[index].element.remove(); + } + + // Remove from data arrays + attendanceData.splice(index, 1); + const filteredIndex = filteredData.findIndex(r => r.id == recordId); + if (filteredIndex > -1) { + filteredData.splice(filteredIndex, 1); + } + + // Update display + updateTable(); + updatePagination(); + + showToast('Record deleted successfully', 'success'); + } + } +} + +function closeRecordModal() { const modal = document.getElementById('recordModal'); if (modal) { - modal.style.display = 'none'; - console.log('❌ Modal closed'); + modal.classList.remove('show'); + setTimeout(() => { + modal.style.display = 'none'; + }, 200); } } -function clearFilters() { - // Get the current URL without query parameters - const baseUrl = window.location.origin + window.location.pathname; - window.location.href = baseUrl; +// Export functionality +function exportAttendance() { + const exportData = filteredData.map(record => ({ + 'Employee ID': record.employeeId, + 'Location': record.location, + 'Event': record.event, + 'Date': record.date, + 'Time': record.time, + 'Device': record.device, + 'Status': record.status + })); - console.log('🔄 Clearing filters and reloading'); + const csv = convertToCSV(exportData); + downloadCSV(csv, `attendance_report_${new Date().toISOString().split('T')[0]}.csv`); + + showToast('Attendance data exported successfully', 'success'); } -// Global event handlers -window.onclick = function(event) { - const modal = document.getElementById('recordModal'); - if (event.target === modal) { - closeModal(); +function convertToCSV(data) { + if (!data.length) return ''; + + const headers = Object.keys(data[0]); + const csvContent = [ + headers.join(','), + ...data.map(row => + headers.map(header => { + const value = row[header]; + // Escape commas and quotes + return typeof value === 'string' && (value.includes(',') || value.includes('"')) + ? `"${value.replace(/"/g, '""')}"` + : value; + }).join(',') + ) + ].join('\n'); + + return csvContent; +} + +function downloadCSV(csv, filename) { + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + + if (link.download !== undefined) { + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', filename); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); } } -// Make functions globally available -window.exportToCSV = exportToCSV; -window.printReport = printReport; -window.viewRecord = viewRecord; -window.closeModal = closeModal; -window.clearFilters = clearFilters; +function refreshReport() { + showToast('Refreshing report...', 'info'); + setTimeout(() => { + window.location.reload(); + }, 500); +} + +// Charts initialization +function initializeCharts() { + loadAttendanceStats(); +} + +function loadAttendanceStats() { + fetch('/api/attendance/stats') + .then(response => response.json()) + .then(data => { + createDailyChart(data.daily_stats || []); + createLocationChart(data.location_stats || []); + }) + .catch(error => { + console.error('Error loading attendance stats:', error); + }); +} + +function createDailyChart(dailyStats) { + const ctx = document.getElementById('dailyChart'); + if (!ctx) return; + + if (dailyChart) { + dailyChart.destroy(); + } + + dailyChart = new Chart(ctx, { + type: 'line', + data: { + labels: dailyStats.map(stat => stat.date), + datasets: [{ + label: 'Check-ins', + data: dailyStats.map(stat => stat.checkins), + borderColor: '#2563eb', + backgroundColor: 'rgba(37, 99, 235, 0.1)', + tension: 0.4, + fill: true + }, { + label: 'Unique Employees', + data: dailyStats.map(stat => stat.employees), + borderColor: '#059669', + backgroundColor: 'rgba(5, 150, 105, 0.1)', + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top' + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1 + } + } + } + } + }); +} + +function createLocationChart(locationStats) { + const ctx = document.getElementById('locationChart'); + if (!ctx) return; + + if (locationChart) { + locationChart.destroy(); + } + + locationChart = new Chart(ctx, { + type: 'doughnut', + data: { + labels: locationStats.map(stat => stat.location), + datasets: [{ + data: locationStats.map(stat => stat.checkins), + backgroundColor: [ + '#2563eb', + '#059669', + '#d97706', + '#dc2626', + '#7c3aed', + '#0891b2', + '#65a30d', + '#c2410c' + ] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right' + } + } + } + }); +} + +// Utility functions +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +function showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.innerHTML = ` +
+ + ${message} +
+ `; + + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + padding: 1rem; + z-index: 1000; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + border-left: 4px solid ${getToastColor(type)}; + max-width: 400px; + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }, 100); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (document.body.contains(toast)) { + document.body.removeChild(toast); + } + }, 300); + }, 3000); +} + +function getToastIcon(type) { + const icons = { + success: 'fa-check-circle', + error: 'fa-exclamation-circle', + warning: 'fa-exclamation-triangle', + info: 'fa-info-circle' + }; + return icons[type] || icons.info; +} + +function getToastColor(type) { + const colors = { + success: '#059669', + error: '#dc2626', + warning: '#d97706', + info: '#0891b2' + }; + return colors[type] || colors.info; +} + +// Global function exports window.sortTable = sortTable; window.changeEntriesPerPage = changeEntriesPerPage; - -console.log('📍 Enhanced Attendance Report JavaScript loaded successfully'); -console.log('🔧 Available functions: exportToCSV, printReport, viewRecord, sortTable'); \ No newline at end of file +window.clearFilters = clearFilters; +window.goToPage = goToPage; +window.viewRecordDetails = viewRecordDetails; +window.editRecord = editRecord; +window.deleteRecord = deleteRecord; +window.closeRecordModal = closeRecordModal; +window.exportAttendance = exportAttendance; +window.refreshReport = refreshReport; \ No newline at end of file diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index ecd9696..62e94af 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -1,5 +1,5 @@ /** - * QR Code Destination Page JavaScript + * QR Code Destination Page JavaScript - Complete with Geolocation * Handles staff check-in functionality and form interactions */ @@ -7,13 +7,29 @@ let isSubmitting = false; let currentTime = new Date(); +// NEW: Geolocation variables +let userLocation = { + latitude: null, + longitude: null, + accuracy: null, + altitude: null, + timestamp: null, + source: 'manual', + address: null +}; +let locationRequestActive = false; +let locationWatchId = null; + // Initialize page when DOM is loaded document.addEventListener('DOMContentLoaded', function() { - console.log('QR Destination page initialized'); + console.log('QR Destination page initialized with geolocation'); initializePage(); setupEventListeners(); startTimeUpdater(); + + // NEW: Initialize geolocation + initializeGeolocation(); }); function initializePage() { @@ -54,6 +70,381 @@ function setupEventListeners() { document.addEventListener('visibilitychange', handleVisibilityChange); } +// NEW: Initialize geolocation functionality +function initializeGeolocation() { + console.log('📍 Initializing geolocation...'); + + if (!navigator.geolocation) { + console.warn('❌ Geolocation not supported'); + showLocationStatus('error', 'Location not supported by this browser'); + return; + } + + console.log('✅ Geolocation supported'); + + // Check permissions first + checkLocationPermission(); + + // Request location + requestUserLocation(); +} + +// NEW: Check location permissions +function checkLocationPermission() { + if (navigator.permissions) { + navigator.permissions.query({name: 'geolocation'}).then(function(result) { + console.log('🔐 Location permission status:', result.state); + + if (result.state === 'granted') { + console.log('✅ Location permission granted'); + } else if (result.state === 'prompt') { + console.log('❓ Location permission will be requested'); + } else if (result.state === 'denied') { + console.log('❌ Location permission denied'); + showLocationStatus('error', 'Location permission denied - enable in browser settings'); + } + }).catch(function(error) { + console.log('⚠️ Permission query failed:', error); + }); + } +} + +// NEW: Request user's current location +function requestUserLocation() { + if (locationRequestActive) { + console.log('⏭️ Location request already active'); + return; + } + + locationRequestActive = true; + showLocationStatus('loading', 'Getting your location...'); + + const options = { + enableHighAccuracy: true, // Use GPS for better accuracy + timeout: 15000, // Wait up to 15 seconds + maximumAge: 300000 // Accept cached location up to 5 minutes old + }; + + console.log('📡 Requesting location with options:', options); + + navigator.geolocation.getCurrentPosition( + handleLocationSuccess, + handleLocationError, + options + ); + + // Set backup timeout + setTimeout(() => { + if (locationRequestActive && !userLocation.latitude) { + console.log('⏰ Location request backup timeout'); + handleLocationError({ code: 3, message: 'Request timed out' }); + } + }, 16000); +} + +// NEW: Handle successful location retrieval +function handleLocationSuccess(position) { + locationRequestActive = false; + + const coords = position.coords; + console.log('✅ Location obtained:', coords); + + // Store location data + userLocation = { + latitude: coords.latitude, + longitude: coords.longitude, + accuracy: coords.accuracy, + altitude: coords.altitude, + timestamp: position.timestamp, + source: 'gps', + address: null + }; + + // Update form fields + updateLocationFormFields(); + + // Update display + updateLocationDisplay(); + + // Show success status + const accuracyText = coords.accuracy ? Math.round(coords.accuracy) : 'unknown'; + const accuracyLevel = getAccuracyLevel(coords.accuracy); + + showLocationStatus('success', + `Location captured (±${accuracyText}m - ${accuracyLevel} accuracy)` + ); + + console.log('📍 Location stored:', userLocation); + + // Try to get address from coordinates + reverseGeocodeLocation(coords.latitude, coords.longitude); + + // Start watching for better accuracy (optional) + startLocationWatching(); +} + +// NEW: Handle location errors +function handleLocationError(error) { + locationRequestActive = false; + + console.error('❌ Location error:', error); + + let message = 'Unable to get location'; + let details = 'Check-in will work without location'; + + switch(error.code) { + case error.PERMISSION_DENIED: + message = 'Location access denied'; + details = 'Enable location permission in browser settings'; + break; + case error.POSITION_UNAVAILABLE: + message = 'Location unavailable'; + details = 'GPS signal is weak or unavailable'; + break; + case error.TIMEOUT: + message = 'Location request timed out'; + details = 'Try refreshing or check connection'; + break; + default: + message = 'Location error occurred'; + details = 'Please try again'; + break; + } + + showLocationStatus('error', `${message} - ${details}`); + + // Set location source to manual + userLocation.source = 'manual'; + updateLocationFormFields(); +} + +// NEW: Start watching location for continuous updates +function startLocationWatching() { + if (locationWatchId !== null) { + console.log('👁️ Already watching location'); + return; + } + + console.log('👁️ Starting location watching for better accuracy...'); + + const options = { + enableHighAccuracy: true, + timeout: 30000, + maximumAge: 60000 + }; + + locationWatchId = navigator.geolocation.watchPosition( + function(position) { + // Only update if accuracy is better + if (!userLocation.accuracy || position.coords.accuracy < userLocation.accuracy) { + console.log('📍 Location updated with better accuracy:', position.coords.accuracy); + handleLocationSuccess(position); + } + }, + function(error) { + console.log('⚠️ Location watch error:', error); + }, + options + ); +} + +// NEW: Stop watching location +function stopLocationWatching() { + if (locationWatchId !== null) { + navigator.geolocation.clearWatch(locationWatchId); + locationWatchId = null; + console.log('⏹️ Stopped watching location'); + } +} + +// NEW: Update form fields with location data +function updateLocationFormFields() { + const fields = { + 'latitude': userLocation.latitude || '', + 'longitude': userLocation.longitude || '', + 'accuracy': userLocation.accuracy || '', + 'altitude': userLocation.altitude || '', + 'locationSource': userLocation.source || 'manual', + 'address': userLocation.address || '' + }; + + Object.keys(fields).forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.value = fields[fieldId]; + } + }); + + console.log('📝 Updated form fields with location data'); +} + +// NEW: Update location display +function updateLocationDisplay() { + if (userLocation.latitude && userLocation.longitude) { + const elements = { + 'displayLatitude': userLocation.latitude.toFixed(6), + 'displayLongitude': userLocation.longitude.toFixed(6), + 'displayAccuracy': userLocation.accuracy ? `±${Math.round(userLocation.accuracy)}m` : 'Unknown', + 'displayAddress': userLocation.address || 'Loading...' + }; + + Object.keys(elements).forEach(elementId => { + const element = document.getElementById(elementId); + if (element) { + element.textContent = elements[elementId]; + } + }); + } +} + +// NEW: Reverse geocode coordinates to get address +function reverseGeocodeLocation(lat, lng) { + console.log('🏠 Getting address from coordinates...'); + + // Try multiple geocoding services for better reliability + const services = [ + { + name: 'BigDataCloud', + url: `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lng}&localityLanguage=en`, + parser: (data) => data.locality || data.city || data.neighbourhood || '' + }, + { + name: 'OpenStreetMap', + url: `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`, + parser: (data) => data.display_name ? data.display_name.split(',')[0] : '' + } + ]; + + tryGeocodingService(0, services); +} + +function tryGeocodingService(index, services) { + if (index >= services.length) { + console.log('⚠️ All geocoding services failed'); + const displayAddress = document.getElementById('displayAddress'); + if (displayAddress) { + displayAddress.textContent = 'Address not available'; + } + return; + } + + const service = services[index]; + + fetch(service.url) + .then(response => response.json()) + .then(data => { + const address = service.parser(data); + + if (address) { + userLocation.address = address; + document.getElementById('address').value = address; + + const displayAddress = document.getElementById('displayAddress'); + if (displayAddress) { + displayAddress.textContent = address; + } + + console.log(`🏠 Address found using ${service.name}:`, address); + return; + } + + // Try next service + tryGeocodingService(index + 1, services); + }) + .catch(error => { + console.log(`⚠️ ${service.name} geocoding failed:`, error); + // Try next service + tryGeocodingService(index + 1, services); + }); +} + +// NEW: Show location status to user +function showLocationStatus(type, message) { + const statusElement = document.getElementById('locationStatus'); + const messageElement = document.getElementById('locationMessage'); + + if (!statusElement || !messageElement) { + console.log('📍 Location status elements not found'); + return; + } + + statusElement.className = `location-status ${type}`; + statusElement.style.display = 'flex'; + + let icon = '📡'; + if (type === 'success') icon = '✅'; + if (type === 'error') icon = '⚠️'; + + messageElement.innerHTML = `${icon} ${message}`; + + // Auto-hide after 8 seconds unless it's loading + if (type !== 'loading') { + setTimeout(() => { + statusElement.style.display = 'none'; + }, 8000); + } +} + +// NEW: Get accuracy level description +function getAccuracyLevel(accuracy) { + if (!accuracy) return 'unknown'; + if (accuracy <= 50) return 'high'; + if (accuracy <= 100) return 'medium'; + return 'low'; +} + +// NEW: Toggle location info display +function toggleLocationInfo() { + const locationInfo = document.getElementById('locationInfo'); + if (!locationInfo) return; + + if (locationInfo.style.display === 'none' || !locationInfo.style.display) { + updateLocationDisplay(); + locationInfo.style.display = 'block'; + } else { + locationInfo.style.display = 'none'; + } +} + +// NEW: Retry location request +function retryLocationRequest() { + console.log('🔄 Retrying location request...'); + + // Stop any existing watch + stopLocationWatching(); + + // Reset location data + userLocation = { + latitude: null, + longitude: null, + accuracy: null, + altitude: null, + timestamp: null, + source: 'manual', + address: null + }; + + // Clear form fields + updateLocationFormFields(); + + // Request location again + requestUserLocation(); +} + +// NEW: Get current location data for external use +function getCurrentLocationData() { + return { + hasLocation: !!(userLocation.latitude && userLocation.longitude), + latitude: userLocation.latitude, + longitude: userLocation.longitude, + accuracy: userLocation.accuracy, + altitude: userLocation.altitude, + source: userLocation.source, + timestamp: userLocation.timestamp, + address: userLocation.address + }; +} + function handleFormSubmit(e) { e.preventDefault(); @@ -67,6 +458,9 @@ function handleFormSubmit(e) { return false; } + // NEW: Ensure location data is up to date before submission + updateLocationFormFields(); + submitCheckin(employeeId); } @@ -118,7 +512,7 @@ function validateEmployeeId() { } if (employeeId.length > 20) { - showValidationError(employeeInput, 'Employee ID must be less than 20 characters'); + showValidationError(employeeInput, 'Employee ID must be 20 characters or less'); return false; } @@ -127,46 +521,66 @@ function validateEmployeeId() { return false; } - // Clear validation error - employeeInput.classList.remove('error'); - employeeInput.classList.add('success'); - hideStatusMessage(); - + clearValidationError(employeeInput); return true; } function isValidEmployeeId(id) { - return /^[A-Za-z0-9]{3,20}$/.test(id); + return /^[A-Za-z0-9]+$/.test(id); } function showValidationError(input, message) { input.classList.add('error'); - input.classList.remove('success'); showStatusMessage(message, 'error'); shakeInput(input); input.focus(); } +function clearValidationError(input) { + input.classList.remove('error'); + input.classList.add('success'); +} + function shakeInput(input) { - input.classList.add('shake'); + input.style.animation = 'shake 0.5s'; setTimeout(() => { - input.classList.remove('shake'); + input.style.animation = ''; }, 500); } function submitCheckin(employeeId) { - if (isSubmitting) return; + console.log('🚀 Submitting check-in for:', employeeId); + + // NEW: Log location data being submitted + const locationData = getCurrentLocationData(); + console.log('📍 Location data:', locationData); isSubmitting = true; - showLoadingState(); - showLoadingOverlay(); + updateSubmitButton(true); + showStatusMessage('Processing check-in...', 'info'); + + // NEW: Ensure all location data is in the form + updateLocationFormFields(); // Prepare form data const formData = new FormData(); formData.append('employee_id', employeeId); - // Submit to server - fetch(`/qr/${window.qrUrl}/checkin`, { + // NEW: Add location data to form submission + formData.append('latitude', userLocation.latitude || ''); + formData.append('longitude', userLocation.longitude || ''); + formData.append('accuracy', userLocation.accuracy || ''); + formData.append('altitude', userLocation.altitude || ''); + formData.append('location_source', userLocation.source || 'manual'); + formData.append('address', userLocation.address || ''); + + // Get the current URL for the check-in endpoint + const currentUrl = window.location.pathname; + const checkinUrl = `${currentUrl}/checkin`; + + console.log('📤 Submitting to:', checkinUrl); + + fetch(checkinUrl, { method: 'POST', body: formData, headers: { @@ -175,235 +589,139 @@ function submitCheckin(employeeId) { }) .then(response => response.json()) .then(data => { - handleCheckinResponse(data); + isSubmitting = false; + updateSubmitButton(false); + + if (data.success) { + showSuccessPage(data); + console.log('✅ Check-in successful:', data); + + // Stop location watching after successful check-in + stopLocationWatching(); + } else { + showStatusMessage(data.message || 'Check-in failed', 'error'); + console.log('❌ Check-in failed:', data.message); + } }) .catch(error => { - console.error('Check-in error:', error); - handleCheckinError('Network error. Please check your connection and try again.'); - }) - .finally(() => { isSubmitting = false; - hideLoadingState(); - hideLoadingOverlay(); + updateSubmitButton(false); + showStatusMessage('Network error. Please check your connection and try again.', 'error'); + console.error('❌ Check-in error:', error); }); } -function handleCheckinResponse(data) { - if (data.success) { - showSuccessCard(data.data); - logSuccessfulCheckin(data.data); - - // Optional: Analytics tracking - if (typeof gtag !== 'undefined') { - gtag('event', 'checkin_success', { - 'location': window.locationName, - 'event_name': window.eventName - }); - } - } else { - handleCheckinError(data.message); - } -} - -function handleCheckinError(message) { - showStatusMessage(message, 'error'); - - // Shake the form to draw attention - const form = document.getElementById('checkinForm'); - if (form) { - form.classList.add('shake'); - setTimeout(() => { - form.classList.remove('shake'); - }, 500); - } - - // Re-focus on input - const employeeInput = document.getElementById('employee_id'); - if (employeeInput) { - employeeInput.focus(); - employeeInput.select(); - } -} - -function showSuccessCard(data) { - // Hide the check-in form +function showSuccessPage(data) { + // Hide the check-in form and location status const checkinCard = document.querySelector('.checkin-card'); - if (checkinCard) { - checkinCard.style.display = 'none'; - } + const locationStatus = document.getElementById('locationStatus'); + const locationInfo = document.getElementById('locationInfo'); + const locationControls = document.querySelector('.location-controls'); - // Populate and show success card + if (checkinCard) checkinCard.style.display = 'none'; + if (locationStatus) locationStatus.style.display = 'none'; + if (locationInfo) locationInfo.style.display = 'none'; + if (locationControls) locationControls.style.display = 'none'; + + // Show success card const successCard = document.getElementById('successCard'); if (successCard) { - document.getElementById('successEmployeeId').textContent = data.employee_id || '-'; - document.getElementById('successLocation').textContent = data.location || '-'; - document.getElementById('successEvent').textContent = data.event || '-'; - document.getElementById('successTime').textContent = data.time || '-'; - document.getElementById('successDate').textContent = data.date || '-'; - successCard.style.display = 'block'; - successCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - - // Optional: Auto-hide success card after some time - setTimeout(() => { - showAutoHideOption(); - }, 10000); // 10 seconds -} - -function showAutoHideOption() { - const successCard = document.getElementById('successCard'); - if (successCard && successCard.style.display !== 'none') { - const actions = successCard.querySelector('.success-actions'); - if (actions && !actions.querySelector('.auto-hide-btn')) { - const autoHideBtn = document.createElement('button'); - autoHideBtn.className = 'btn btn-outline auto-hide-btn'; - autoHideBtn.innerHTML = ' Auto-hide in 30s'; - actions.appendChild(autoHideBtn); + + // Populate success details + const elements = { + 'successEmployeeId': data.employee_id || '-', + 'successLocation': data.location || window.locationName || '-', + 'successEvent': data.event || window.eventName || '-', + 'successTime': data.time || new Date().toLocaleTimeString(), + 'successDate': data.date || new Date().toLocaleDateString() + }; + + Object.keys(elements).forEach(elementId => { + const element = document.getElementById(elementId); + if (element) { + element.textContent = elements[elementId]; + } + }); + + // NEW: Show location info in success card if available + const successLocationInfo = document.getElementById('successLocationInfo'); + const successGpsInfo = document.getElementById('successGpsInfo'); + + if (data.has_location && userLocation.latitude && userLocation.longitude) { + let locationText = `Captured (±${Math.round(userLocation.accuracy || 0)}m)`; + if (userLocation.address) { + locationText += ` - ${userLocation.address}`; + } - startCountdown(30, () => { - checkInAnother(); - }); - } - } -} - -function startCountdown(seconds, callback) { - const countdownElement = document.getElementById('countdown'); - let remaining = seconds; - - const interval = setInterval(() => { - remaining--; - if (countdownElement) { - countdownElement.textContent = remaining; + if (successGpsInfo) successGpsInfo.textContent = locationText; + if (successLocationInfo) successLocationInfo.style.display = 'block'; } - if (remaining <= 0) { - clearInterval(interval); - callback(); - } - }, 1000); + // Scroll to success card + successCard.scrollIntoView({ behavior: 'smooth' }); + } + + // Auto-refresh page after 30 seconds + setTimeout(() => { + console.log('🔄 Auto-refreshing page...'); + window.location.reload(); + }, 30000); } -function checkInAnother() { - // Show the check-in form again - const checkinCard = document.querySelector('.checkin-card'); - const successCard = document.getElementById('successCard'); +function showStatusMessage(message, type) { + const statusElement = document.getElementById('statusMessage'); + if (!statusElement) return; - if (checkinCard) { - checkinCard.style.display = 'block'; - } + statusElement.className = `status-message ${type}`; + statusElement.innerHTML = ` +
+ + ${message} +
+ `; + statusElement.style.display = 'block'; - if (successCard) { - successCard.style.display = 'none'; - } - - // Reset form - const form = document.getElementById('checkinForm'); - if (form) { - form.reset(); - } - - // Clear validation states - const employeeInput = document.getElementById('employee_id'); - if (employeeInput) { - employeeInput.classList.remove('error', 'success'); - employeeInput.focus(); - } - - hideStatusMessage(); - - // Scroll back to form - checkinCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); -} - -function showLoadingState() { - const btn = document.querySelector('.btn-primary'); - if (btn) { - const content = btn.querySelector('.btn-content'); - const loader = btn.querySelector('.btn-loader'); - - if (content) content.style.display = 'none'; - if (loader) loader.style.display = 'flex'; - - btn.disabled = true; - } -} - -function hideLoadingState() { - const btn = document.querySelector('.btn-primary'); - if (btn) { - const content = btn.querySelector('.btn-content'); - const loader = btn.querySelector('.btn-loader'); - - if (content) content.style.display = 'flex'; - if (loader) loader.style.display = 'none'; - - btn.disabled = false; - } -} - -function showLoadingOverlay() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.style.display = 'flex'; + // Auto-hide info messages + if (type === 'info') { setTimeout(() => { - overlay.classList.add('show'); - }, 10); - } -} - -function hideLoadingOverlay() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.classList.remove('show'); - setTimeout(() => { - overlay.style.display = 'none'; - }, 200); - } -} - -function showStatusMessage(message, type = 'info') { - const statusDiv = document.getElementById('statusMessage'); - if (statusDiv) { - statusDiv.textContent = message; - statusDiv.className = `status-message ${type}`; - statusDiv.style.display = 'block'; - - // Auto-hide success messages - if (type === 'success') { - setTimeout(() => { - hideStatusMessage(); - }, 5000); - } - - // Scroll to message - statusDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); + statusElement.style.display = 'none'; + }, 3000); } } function hideStatusMessage() { - const statusDiv = document.getElementById('statusMessage'); - if (statusDiv) { - statusDiv.style.display = 'none'; + const statusElement = document.getElementById('statusMessage'); + if (statusElement) { + statusElement.style.display = 'none'; } } -function updateCurrentTime() { - const timeElement = document.getElementById('currentTime'); - if (timeElement) { - const now = new Date(); - const options = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }; - - timeElement.textContent = now.toLocaleDateString('en-US', options); +function getStatusIcon(type) { + switch(type) { + case 'success': return 'fa-check-circle'; + case 'error': return 'fa-exclamation-triangle'; + case 'info': return 'fa-info-circle'; + case 'warning': return 'fa-exclamation-circle'; + default: return 'fa-info-circle'; + } +} + +function updateSubmitButton(isLoading) { + const submitBtn = document.getElementById('submitBtn') || document.querySelector('button[type="submit"]'); + if (!submitBtn) return; + + const btnContent = submitBtn.querySelector('.btn-content'); + const btnLoader = submitBtn.querySelector('.btn-loader'); + + if (isLoading) { + submitBtn.disabled = true; + if (btnContent) btnContent.style.display = 'none'; + if (btnLoader) btnLoader.style.display = 'flex'; + } else { + submitBtn.disabled = false; + if (btnContent) btnContent.style.display = 'flex'; + if (btnLoader) btnLoader.style.display = 'none'; } } @@ -412,179 +730,65 @@ function startTimeUpdater() { setInterval(updateCurrentTime, 1000); } +function updateCurrentTime() { + const now = new Date(); + const timeString = now.toLocaleString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + const timeElement = document.getElementById('currentTime'); + if (timeElement) { + timeElement.textContent = timeString; + } + + currentTime = now; +} + function handleVisibilityChange() { - if (document.hidden) { - // Page is hidden - pause operations - console.log('Page hidden - pausing operations'); - } else { - // Page is visible - resume operations - console.log('Page visible - resuming operations'); + if (!document.hidden) { + // Page became visible, update time immediately updateCurrentTime(); - // Re-focus on input if form is visible - const checkinCard = document.querySelector('.checkin-card'); - const employeeInput = document.getElementById('employee_id'); - - if (checkinCard && checkinCard.style.display !== 'none' && employeeInput) { - setTimeout(() => { - employeeInput.focus(); - }, 100); + // NEW: Request location again if we don't have it and haven't submitted yet + if (!userLocation.latitude && !isSubmitting) { + console.log('🔄 Page visible again, retrying location...'); + setTimeout(requestUserLocation, 1000); } } } -function logSuccessfulCheckin(data) { - console.log('Successful check-in:', { - employee_id: data.employee_id, - location: data.location, - event: data.event, - time: data.time, - date: data.date - }); -} - -// Utility functions -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -function throttle(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - } -} - -// Export functions for global access -window.checkInAnother = checkInAnother; -window.validateEmployeeId = validateEmployeeId; - -// Service Worker registration for offline support (optional) -if ('serviceWorker' in navigator) { - window.addEventListener('load', function() { - navigator.serviceWorker.register('/sw.js') - .then(function(registration) { - console.log('ServiceWorker registration successful'); - }) - .catch(function(err) { - console.log('ServiceWorker registration failed: ', err); - }); - }); -} - -// Error handling for unhandled promises -window.addEventListener('unhandledrejection', function(event) { - console.error('Unhandled promise rejection:', event.reason); - handleCheckinError('An unexpected error occurred. Please try again.'); - event.preventDefault(); -}); - -// Handle online/offline status -window.addEventListener('online', function() { - showStatusMessage('Connection restored', 'success'); -}); - -window.addEventListener('offline', function() { - showStatusMessage('No internet connection. Please check your network.', 'warning'); -}); - -// Performance monitoring -if ('performance' in window) { - window.addEventListener('load', function() { - setTimeout(function() { - const perfData = performance.getEntriesByType('navigation')[0]; - console.log('Page load time:', perfData.loadEventEnd - perfData.loadEventStart, 'ms'); - }, 0); - }); -} - -// Accessibility enhancements -document.addEventListener('keydown', function(e) { - // Escape key to reset form - if (e.key === 'Escape') { - const successCard = document.getElementById('successCard'); - if (successCard && successCard.style.display !== 'none') { - checkInAnother(); - } else { - // Reset form - const form = document.getElementById('checkinForm'); - if (form) { - form.reset(); - hideStatusMessage(); - - const employeeInput = document.getElementById('employee_id'); - if (employeeInput) { - employeeInput.classList.remove('error', 'success'); - employeeInput.focus(); - } - } - } - } +function checkInAnother() { + // Stop location watching + stopLocationWatching(); - // Ctrl+R to refresh (prevent default and reload page cleanly) - if ((e.ctrlKey || e.metaKey) && e.key === 'r') { - e.preventDefault(); - window.location.reload(); - } -}); - -// Touch device optimizations -if ('ontouchstart' in window) { - // Add touch-friendly classes - document.body.classList.add('touch-device'); - - // Prevent zoom on input focus for iOS - const inputs = document.querySelectorAll('input[type="text"]'); - inputs.forEach(input => { - input.addEventListener('focus', function() { - const viewport = document.querySelector('meta[name="viewport"]'); - if (viewport) { - viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); - } - }); - - input.addEventListener('blur', function() { - const viewport = document.querySelector('meta[name="viewport"]'); - if (viewport) { - viewport.setAttribute('content', 'width=device-width, initial-scale=1.0'); - } - }); - }); + // Reload page + window.location.reload(); } -// Auto-refresh page if idle for too long (optional) -let idleTimer; -const IDLE_TIME = 30 * 60 * 1000; // 30 minutes - -function resetIdleTimer() { - clearTimeout(idleTimer); - idleTimer = setTimeout(() => { - if (confirm('This page has been idle for 30 minutes. Would you like to refresh it?')) { - window.location.reload(); - } else { - resetIdleTimer(); // Reset timer if user chooses not to refresh - } - }, IDLE_TIME); +// NEW: Cleanup function for page unload +function cleanup() { + stopLocationWatching(); + console.log('🧹 Cleaned up geolocation resources'); } -// Track user activity -['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => { - document.addEventListener(event, resetIdleTimer, true); -}); +// NEW: Setup cleanup handlers +window.addEventListener('beforeunload', cleanup); +window.addEventListener('pagehide', cleanup); -// Initialize idle timer -resetIdleTimer(); \ No newline at end of file +// NEW: Export geolocation functions for global use +window.requestUserLocation = requestUserLocation; +window.getCurrentLocationData = getCurrentLocationData; +window.retryLocationRequest = retryLocationRequest; +window.toggleLocationInfo = toggleLocationInfo; +window.stopLocationWatching = stopLocationWatching; +window.startLocationWatching = startLocationWatching; + +console.log('📍 QR Destination with Geolocation loaded successfully!'); +console.log('🔧 Available functions: requestUserLocation(), getCurrentLocationData(), retryLocationRequest(), toggleLocationInfo()'); +console.log('📊 Location tracking ready for check-ins!'); \ No newline at end of file diff --git a/templates/attendance_report.html b/templates/attendance_report.html index c8ac4a9..1b31952 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -1,122 +1,12 @@ {% extends "base.html" %} -{% block title %}Attendance Report{% endblock %} +{% block title %}Attendance Report - QR Code Management{% endblock %} -{% block extra_css %} +{% block extra_head %} + - + + {% endblock %} {% block content %} @@ -128,66 +18,65 @@ Attendance Report -

Comprehensive attendance tracking with location data for {{ current_date_formatted }}

+

Monitor and analyze staff attendance across all locations

+
- - - - - Back to Dashboard -
- +
-
- -
-
-

{{ stats.total_checkins }}

-

Total Check-ins

-
-
- -
-

{{ stats.unique_employees }}

-

Unique Employees

+

{{ stats.total_checkins or 0 }}

+

Total Check-ins

+ All time
- -
+ +
- +
-

{{ stats.today_checkins }}

-

Today's Check-ins

+

{{ stats.unique_employees or 0 }}

+

Unique Employees

+ Have checked in
- - -
+ +
-

{{ stats.checkins_with_location }}

-

With GPS Location

- {{ location_coverage }}% coverage +

{{ stats.active_locations or 0 }}

+

Active Locations

+ With check-ins +
+
+ +
+
+ +
+
+

{{ stats.today_checkins or 0 }}

+

Today's Check-ins

+ {{ current_date_formatted or 'Today' }}
@@ -203,7 +92,7 @@
-
+