diff --git a/app.py b/app.py index 788c363..2f92dd6 100644 --- a/app.py +++ b/app.py @@ -183,6 +183,7 @@ 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""" @@ -626,6 +627,68 @@ 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): @@ -1368,40 +1431,97 @@ def toggle_qr_status_api(qr_id): return redirect(url_for('dashboard')) @app.route('/attendance') -#@admin_required +@admin_required def attendance_report(): - """Attendance report page (Admin only)""" + """Enhanced attendance report page with location data""" try: # Get filter parameters date_filter = request.args.get('date', '') location_filter = request.args.get('location', '') employee_filter = request.args.get('employee', '') - # Base query using the view - query = db.session.execute(text("SELECT * FROM attendance_report WHERE 1=1")) + # 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 + """ - # Apply filters (you can enhance this with proper SQLAlchemy filtering) + # 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) 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()] - # Get attendance statistics + # Enhanced statistics including location data 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 + 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 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') @@ -1410,6 +1530,7 @@ 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, @@ -1418,6 +1539,8 @@ 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')) @@ -1656,8 +1779,41 @@ 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 new file mode 100644 index 0000000..94284b5 --- /dev/null +++ b/location_migration.py @@ -0,0 +1,258 @@ +#!/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 db4447d..f3174eb 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -1,8 +1,3 @@ -/** - * Attendance Report Page Styles - * static/css/attendance.css - */ - /* Page Container */ .attendance-page { max-width: 1400px; @@ -109,7 +104,43 @@ } .stat-card.warning::before { - background: linear-gradient(90deg, var(--warning-color), #b45309); + 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; } .stat-card:hover { @@ -129,20 +160,24 @@ flex-shrink: 0; } -.stat-card.primary .stat-icon { +.stat-icon.primary { background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); } -.stat-card.success .stat-icon { +.stat-icon.success { background: linear-gradient(135deg, var(--success-color), #047857); } -.stat-card.info .stat-icon { +.stat-icon.info { background: linear-gradient(135deg, var(--info-color), #0369a1); } -.stat-card.warning .stat-icon { - background: linear-gradient(135deg, var(--warning-color), #b45309); +.stat-icon.warning { + background: linear-gradient(135deg, var(--warning-color), #d97706); +} + +.stat-icon.danger { + background: linear-gradient(135deg, var(--danger-color), #b91c1c); } .stat-content { @@ -154,20 +189,13 @@ 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); - font-style: italic; + margin: 0; + font-weight: 500; } /* Filters Section */ @@ -183,7 +211,7 @@ } .filters-header { - padding: var(--spacing-6); + padding: var(--spacing-6) var(--spacing-6) var(--spacing-4); border-bottom: 1px solid var(--gray-200); background: var(--gray-50); } @@ -202,14 +230,11 @@ 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; } @@ -220,9 +245,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); @@ -290,6 +315,12 @@ color: var(--primary-color); } +.record-count { + font-size: 14px; + color: #6c757d; + font-weight: normal; +} + .table-controls { display: flex; align-items: center; @@ -354,12 +385,46 @@ 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); } @@ -368,6 +433,20 @@ 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; @@ -430,6 +509,129 @@ 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; @@ -462,20 +664,36 @@ /* Action Buttons */ .record-actions { display: flex; - gap: var(--spacing-1); + gap: 4px; + align-items: center; + justify-content: flex-start; } .action-btn { - width: 32px; - height: 32px; - border: none; - border-radius: var(--radius); - cursor: pointer; - transition: var(--transition); - display: flex; + display: inline-flex; align-items: center; justify-content: center; - font-size: var(--font-size-xs); + 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; } .btn-view { @@ -511,28 +729,28 @@ /* Empty State */ .empty-state { text-align: center; - padding: var(--spacing-20); - color: var(--gray-500); + padding: 60px 20px; + color: #6b7280; } -.empty-icon { - font-size: 4rem; - color: var(--gray-300); - margin-bottom: var(--spacing-4); +.empty-state i { + font-size: 4em; + margin-bottom: 20px; + color: #d1d5db; } .empty-state h3 { - font-size: var(--font-size-xl); - color: var(--gray-700); - margin-bottom: var(--spacing-2); + font-size: 1.5em; + margin-bottom: 10px; + color: #374151; } .empty-state p { - font-size: var(--font-size-base); - margin-bottom: var(--spacing-6); + margin-bottom: 20px; max-width: 400px; margin-left: auto; margin-right: auto; + line-height: 1.5; } /* Pagination */ @@ -634,65 +852,63 @@ /* Modal Styles */ .modal { - display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); - z-index: var(--z-modal); - backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; } .modal-content { - 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%; + background: white; + border-radius: 12px; max-width: 600px; + width: 90%; 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: var(--spacing-6); - border-bottom: 1px solid var(--gray-200); + padding: 20px; + border-bottom: 1px solid #e5e7eb; + background: #f9fafb; display: flex; justify-content: space-between; align-items: center; - background: var(--gray-50); } .modal-header h3 { margin: 0; - font-size: var(--font-size-xl); - font-weight: 600; - color: var(--gray-900); + color: #111827; + display: flex; + align-items: center; + gap: 8px; } .modal-close { background: none; border: none; - font-size: var(--font-size-xl); + font-size: 18px; cursor: pointer; - color: var(--gray-500); - padding: var(--spacing-2); - border-radius: var(--radius); - transition: var(--transition); + color: #6b7280; + padding: 4px; + border-radius: 4px; + transition: all 0.2s ease; } .modal-close:hover { - background: var(--gray-200); - color: var(--gray-700); + background: #e5e7eb; + color: #374151; } .modal-body { - padding: var(--spacing-6); + padding: 20px; max-height: 60vh; overflow-y: auto; } @@ -706,7 +922,81 @@ 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); @@ -765,57 +1055,20 @@ .record-actions { flex-direction: column; } -} -@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 { + /* 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) { display: none; } - - .attendance-table-section { - box-shadow: none; - border: 1px solid #ccc; + + .location-data { + font-size: 8px; + padding: 1px 3px; } - - .action-btn { - display: none; - } - - .pagination-container { - display: none; - } -} \ No newline at end of file + + .coordinates { + display: none; /* Hide coordinates on mobile */ + } \ No newline at end of file diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 6f7ccf3..0a23fb3 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -1,6 +1,6 @@ /** - * Attendance Report JavaScript - * Handles filtering, sorting, pagination, and analytics for attendance data + * Enhanced Attendance Report JavaScript with Location Support + * Fixed version - removes undefined function calls */ // Global variables @@ -11,144 +11,163 @@ let sortDirection = 'asc'; let attendanceData = []; let filteredData = []; -// Charts +// Charts (if you want to add them later) let dailyChart = null; let locationChart = null; // Initialize page when DOM is loaded document.addEventListener('DOMContentLoaded', function() { - console.log('Attendance Report page initialized'); + console.log('Enhanced Attendance Report page initialized'); initializeReport(); - loadAttendanceData(); - initializeCharts(); setupEventListeners(); + + // Initialize location-specific features + initializeLocationFeatures(); }); function initializeReport() { - // Load data from table + console.log('📊 Initializing attendance report...'); + + // Load data from existing table loadTableData(); - // Initialize pagination + // Initialize pagination if needed updatePagination(); - // Apply initial filters if any + // Apply any initial filters applyFilters(); + + console.log('✅ Report initialized successfully'); } function loadTableData() { const table = document.getElementById('attendanceTable'); - 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]; + if (!table) { + console.log('⚠️ Attendance table not found'); + return; } + + 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 change + // Entries per page selector const entriesSelect = document.getElementById('entriesPerPage'); if (entriesSelect) { entriesSelect.addEventListener('change', changeEntriesPerPage); } - // Filter form - const filtersForm = document.getElementById('filtersForm'); - if (filtersForm) { - filtersForm.addEventListener('submit', function(e) { - e.preventDefault(); - applyFilters(); + // 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); }); - } + }); - // 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); - } + // Add click handlers for view record buttons + document.querySelectorAll('.action-btn[title*="View"]').forEach(btn => { + btn.addEventListener('click', function(e) { + e.preventDefault(); + const recordId = this.closest('tr').dataset.recordId; + if (recordId) { + viewRecord(recordId); + } + }); + }); } function changeEntriesPerPage() { const select = document.getElementById('entriesPerPage'); + if (!select) return; + entriesPerPage = select.value === 'all' ? filteredData.length : parseInt(select.value); currentPage = 1; - updateTable(); + + console.log(`📄 Changed entries per page to: ${entriesPerPage}`); + displayPage(); updatePagination(); } -function applyFilters() { - const dateFilter = document.getElementById('date')?.value || ''; - const locationFilter = document.getElementById('location')?.value || ''; - const employeeFilter = document.getElementById('employee')?.value.toLowerCase() || ''; +function handleSearch(event) { + const searchTerm = event.target.value.toLowerCase().trim(); - 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; - updateTable(); - updatePagination(); - updateFilterStats(); -} - -function clearFilters() { - // Clear form inputs - const form = document.getElementById('filtersForm'); - if (form) { - form.reset(); + 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) + ); } - // Reset filtered data - filteredData = [...attendanceData]; currentPage = 1; - - // Update display - updateTable(); + displayPage(); updatePagination(); - updateFilterStats(); - // Update URL without filters - const url = new URL(window.location); - url.search = ''; - window.history.pushState({}, '', url); + console.log(`🔍 Search results: ${filteredData.length} records found`); +} + +function handleFilterSubmit(event) { + // Let the form submit normally to reload with filters + console.log('🔽 Applying filters...'); } 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 { @@ -156,568 +175,251 @@ function sortTable(columnIndex) { sortDirection = 'asc'; } - 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 { + // 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 aVal = aVal.toString().toLowerCase(); bVal = bVal.toString().toLowerCase(); - } - - if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; - if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; - return 0; - }); - - updateTable(); - updateSortIndicators(columnIndex); -} - -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'}`; + + if (sortDirection === 'asc') { + return aVal.localeCompare(bVal); } else { - icon.className = 'fas fa-sort'; + 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' + }; + + return sortKeys[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 updateTable() { - const tbody = document.querySelector('#attendanceTable tbody'); +function displayPage() { + const table = document.getElementById('attendanceTable'); + if (!table) return; + + const tbody = table.querySelector('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 = entriesPerPage === filteredData.length ? - filteredData.length : - Math.min(startIndex + entriesPerPage, filteredData.length); + const endIndex = startIndex + (entriesPerPage === filteredData.length ? filteredData.length : entriesPerPage); - // Hide all rows first - attendanceData.forEach(record => { - if (record.element) { - record.element.style.display = 'none'; + // Show relevant rows + for (let i = startIndex; i < endIndex && i < filteredData.length; i++) { + const record = filteredData[i]; + if (record.row) { + record.row.style.display = ''; } - }); - - // 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'; } + + console.log(`📄 Displaying records ${startIndex + 1}-${Math.min(endIndex, filteredData.length)} of ${filteredData.length}`); } function updatePagination() { - const container = document.getElementById('paginationContainer'); - if (!container) return; + // This is a placeholder for pagination controls + // You can implement pagination UI here if needed const totalPages = Math.ceil(filteredData.length / entriesPerPage); - - if (totalPages <= 1) { - container.innerHTML = ''; + 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'); return; } - 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; + console.log('📄 CSV exported successfully'); } -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 printReport() { + window.print(); + console.log('🖨️ Print dialog opened'); } -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; +function viewRecord(recordId) { + console.log('👁️ Viewing record:', recordId); const modal = document.getElementById('recordModal'); - const modalTitle = document.getElementById('modalTitle'); - const modalBody = document.getElementById('modalBody'); + const detailsDiv = document.getElementById('recordDetails'); - if (!modal || !modalTitle || !modalBody) return; + if (!modal || !detailsDiv) { + console.error('❌ Modal elements not found'); + return; + } - modalTitle.textContent = `Attendance Record - ${record.employeeId}`; + // Find the record row + const row = document.querySelector(`tr[data-record-id="${recordId}"]`); + if (!row) { + console.error('❌ Record row not found'); + return; + } - modalBody.innerHTML = ` -
-
-
- Employee ID: - ${record.employeeId} -
-
- Location: - ${record.location} -
-
- Event: - ${record.event} -
-
- Date: - ${record.date} -
-
- Time: - ${record.time} -
-
- Device: - ${record.device} -
-
- Status: - - ${record.status} - -
+ 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'}
`; modal.style.display = 'flex'; - setTimeout(() => modal.classList.add('show'), 10); + console.log('✅ Record modal opened'); } -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() { +function closeModal() { const modal = document.getElementById('recordModal'); if (modal) { - modal.classList.remove('show'); - setTimeout(() => { - modal.style.display = 'none'; - }, 200); + modal.style.display = 'none'; + console.log('❌ Modal closed'); } } -// 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 - })); +function clearFilters() { + // Get the current URL without query parameters + const baseUrl = window.location.origin + window.location.pathname; + window.location.href = baseUrl; - const csv = convertToCSV(exportData); - downloadCSV(csv, `attendance_report_${new Date().toISOString().split('T')[0]}.csv`); - - showToast('Attendance data exported successfully', 'success'); + console.log('🔄 Clearing filters and reloading'); } -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); +// Global event handlers +window.onclick = function(event) { + const modal = document.getElementById('recordModal'); + if (event.target === modal) { + closeModal(); } } -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 +// Make functions globally available +window.exportToCSV = exportToCSV; +window.printReport = printReport; +window.viewRecord = viewRecord; +window.closeModal = closeModal; +window.clearFilters = clearFilters; window.sortTable = sortTable; window.changeEntriesPerPage = changeEntriesPerPage; -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 + +console.log('📍 Enhanced Attendance Report JavaScript loaded successfully'); +console.log('🔧 Available functions: exportToCSV, printReport, viewRecord, sortTable'); \ No newline at end of file diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index 62e94af..ecd9696 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -1,5 +1,5 @@ /** - * QR Code Destination Page JavaScript - Complete with Geolocation + * QR Code Destination Page JavaScript * Handles staff check-in functionality and form interactions */ @@ -7,29 +7,13 @@ 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 with geolocation'); + console.log('QR Destination page initialized'); initializePage(); setupEventListeners(); startTimeUpdater(); - - // NEW: Initialize geolocation - initializeGeolocation(); }); function initializePage() { @@ -70,381 +54,6 @@ 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(); @@ -458,9 +67,6 @@ function handleFormSubmit(e) { return false; } - // NEW: Ensure location data is up to date before submission - updateLocationFormFields(); - submitCheckin(employeeId); } @@ -512,7 +118,7 @@ function validateEmployeeId() { } if (employeeId.length > 20) { - showValidationError(employeeInput, 'Employee ID must be 20 characters or less'); + showValidationError(employeeInput, 'Employee ID must be less than 20 characters'); return false; } @@ -521,66 +127,46 @@ function validateEmployeeId() { return false; } - clearValidationError(employeeInput); + // Clear validation error + employeeInput.classList.remove('error'); + employeeInput.classList.add('success'); + hideStatusMessage(); + return true; } function isValidEmployeeId(id) { - return /^[A-Za-z0-9]+$/.test(id); + return /^[A-Za-z0-9]{3,20}$/.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.style.animation = 'shake 0.5s'; + input.classList.add('shake'); setTimeout(() => { - input.style.animation = ''; + input.classList.remove('shake'); }, 500); } function submitCheckin(employeeId) { - console.log('🚀 Submitting check-in for:', employeeId); - - // NEW: Log location data being submitted - const locationData = getCurrentLocationData(); - console.log('📍 Location data:', locationData); + if (isSubmitting) return; isSubmitting = true; - updateSubmitButton(true); - showStatusMessage('Processing check-in...', 'info'); - - // NEW: Ensure all location data is in the form - updateLocationFormFields(); + showLoadingState(); + showLoadingOverlay(); // Prepare form data const formData = new FormData(); formData.append('employee_id', employeeId); - // 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, { + // Submit to server + fetch(`/qr/${window.qrUrl}/checkin`, { method: 'POST', body: formData, headers: { @@ -589,139 +175,235 @@ function submitCheckin(employeeId) { }) .then(response => response.json()) .then(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); - } + handleCheckinResponse(data); }) .catch(error => { + console.error('Check-in error:', error); + handleCheckinError('Network error. Please check your connection and try again.'); + }) + .finally(() => { isSubmitting = false; - updateSubmitButton(false); - showStatusMessage('Network error. Please check your connection and try again.', 'error'); - console.error('❌ Check-in error:', error); + hideLoadingState(); + hideLoadingOverlay(); }); } -function showSuccessPage(data) { - // Hide the check-in form and location status - const checkinCard = document.querySelector('.checkin-card'); - const locationStatus = document.getElementById('locationStatus'); - const locationInfo = document.getElementById('locationInfo'); - const locationControls = document.querySelector('.location-controls'); - - 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) { - successCard.style.display = 'block'; +function handleCheckinResponse(data) { + if (data.success) { + showSuccessCard(data.data); + logSuccessfulCheckin(data.data); - // 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}`; - } - - if (successGpsInfo) successGpsInfo.textContent = locationText; - if (successLocationInfo) successLocationInfo.style.display = 'block'; + // Optional: Analytics tracking + if (typeof gtag !== 'undefined') { + gtag('event', 'checkin_success', { + 'location': window.locationName, + 'event_name': window.eventName + }); } - - // Scroll to success card - successCard.scrollIntoView({ behavior: 'smooth' }); + } else { + handleCheckinError(data.message); } - - // Auto-refresh page after 30 seconds - setTimeout(() => { - console.log('🔄 Auto-refreshing page...'); - window.location.reload(); - }, 30000); } -function showStatusMessage(message, type) { - const statusElement = document.getElementById('statusMessage'); - if (!statusElement) return; +function handleCheckinError(message) { + showStatusMessage(message, 'error'); - statusElement.className = `status-message ${type}`; - statusElement.innerHTML = ` -
- - ${message} -
- `; - statusElement.style.display = 'block'; - - // Auto-hide info messages - if (type === 'info') { + // Shake the form to draw attention + const form = document.getElementById('checkinForm'); + if (form) { + form.classList.add('shake'); setTimeout(() => { - statusElement.style.display = 'none'; - }, 3000); + 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 + const checkinCard = document.querySelector('.checkin-card'); + if (checkinCard) { + checkinCard.style.display = 'none'; + } + + // Populate and 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); + + 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 (remaining <= 0) { + clearInterval(interval); + callback(); + } + }, 1000); +} + +function checkInAnother() { + // Show the check-in form again + const checkinCard = document.querySelector('.checkin-card'); + const successCard = document.getElementById('successCard'); + + if (checkinCard) { + checkinCard.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'; + 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' }); } } function hideStatusMessage() { - const statusElement = document.getElementById('statusMessage'); - if (statusElement) { - statusElement.style.display = 'none'; + const statusDiv = document.getElementById('statusMessage'); + if (statusDiv) { + statusDiv.style.display = 'none'; } } -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'; +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); } } @@ -730,65 +412,179 @@ 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 became visible, update time immediately + 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'); updateCurrentTime(); - // 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); + // 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); } } } -function checkInAnother() { - // Stop location watching - stopLocationWatching(); +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(); + } + } + } + } - // Reload page - window.location.reload(); + // 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'); + } + }); + }); } -// NEW: Cleanup function for page unload -function cleanup() { - stopLocationWatching(); - console.log('🧹 Cleaned up geolocation resources'); +// 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: Setup cleanup handlers -window.addEventListener('beforeunload', cleanup); -window.addEventListener('pagehide', cleanup); +// Track user activity +['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => { + document.addEventListener(event, resetIdleTimer, true); +}); -// 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 +// Initialize idle timer +resetIdleTimer(); \ No newline at end of file diff --git a/templates/attendance_report.html b/templates/attendance_report.html index 1b31952..c8ac4a9 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -1,12 +1,122 @@ {% extends "base.html" %} -{% block title %}Attendance Report - QR Code Management{% endblock %} +{% block title %}Attendance Report{% endblock %} -{% block extra_head %} - +{% block extra_css %} - - + {% endblock %} {% block content %} @@ -18,65 +128,66 @@ Attendance Report -

Monitor and analyze staff attendance across all locations

+

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

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

{{ stats.total_checkins or 0 }}

-

Total Check-ins

- All time -
-
- -
-

{{ stats.unique_employees or 0 }}

-

Unique Employees

- Have checked in +

{{ stats.total_checkins }}

+

Total Check-ins

- -
+ +
- +
-

{{ stats.active_locations or 0 }}

-

Active Locations

- With check-ins +

{{ stats.unique_employees }}

+

Unique Employees

- -
+ +
-

{{ stats.today_checkins or 0 }}

+

{{ stats.today_checkins }}

Today's Check-ins

- {{ current_date_formatted or 'Today' }} +
+
+ + +
+
+ +
+
+

{{ stats.checkins_with_location }}

+

With GPS Location

+ {{ location_coverage }}% coverage
@@ -92,7 +203,7 @@
-
+