diff --git a/app.py b/app.py index 7aacc0c..26b5221 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify +from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, send_file from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from functools import wraps @@ -3434,6 +3434,310 @@ def attendance_stats_api(): print(f"Error fetching attendance stats: {e}") return jsonify({'error': 'Failed to fetch attendance statistics'}), 500 +@app.route('/export-configuration') +@admin_required +def export_configuration(): + """Route to display export configuration page""" + try: + print("📊 Export configuration route accessed") + + # Log export configuration access using your existing logger + try: + logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page") + except Exception as log_error: + print(f"⚠️ Logging error (non-critical): {log_error}") + + # Get current filters from session or request args + filters = { + 'date_from': request.args.get('date_from', ''), + 'date_to': request.args.get('date_to', ''), + 'location_filter': request.args.get('location', ''), + 'employee_filter': request.args.get('employee', '') + } + + print(f"📊 Filters: {filters}") + + # Check if location accuracy feature exists + try: + has_location_accuracy = check_location_accuracy_column_exists() + except Exception as e: + print(f"⚠️ Error checking location accuracy column: {e}") + has_location_accuracy = False + + # Define all available columns with their default settings + available_columns = [ + {'key': 'employee_id', 'label': 'Employee ID', 'default_name': 'Employee ID', 'enabled': True}, + {'key': 'location_name', 'label': 'Location', 'default_name': 'Location', 'enabled': True}, + {'key': 'status', 'label': 'Event', 'default_name': 'Event', 'enabled': True}, + {'key': 'check_in_date', 'label': 'Date', 'default_name': 'Date', 'enabled': True}, + {'key': 'check_in_time', 'label': 'Time', 'default_name': 'Time', 'enabled': True}, + {'key': 'qr_address', 'label': 'QR Address', 'default_name': 'QR Code Address', 'enabled': False}, + {'key': 'address', 'label': 'Check-in Address', 'default_name': 'Check-in Address', 'enabled': False}, + {'key': 'device_info', 'label': 'Device', 'default_name': 'Device Information', 'enabled': False}, + {'key': 'ip_address', 'label': 'IP Address', 'default_name': 'IP Address', 'enabled': False}, + {'key': 'user_agent', 'label': 'User Agent', 'default_name': 'Browser/User Agent', 'enabled': False}, + {'key': 'latitude', 'label': 'Latitude', 'default_name': 'GPS Latitude', 'enabled': False}, + {'key': 'longitude', 'label': 'Longitude', 'default_name': 'GPS Longitude', 'enabled': False}, + {'key': 'accuracy', 'label': 'GPS Accuracy', 'default_name': 'GPS Accuracy (meters)', 'enabled': False}, + ] + + # Add location accuracy column if feature exists + if has_location_accuracy: + available_columns.append({ + 'key': 'location_accuracy', + 'label': 'Location Accuracy', + 'default_name': 'Location Accuracy (miles)', + 'enabled': False + }) + + print(f"📊 Rendering export configuration with {len(available_columns)} columns") + + return render_template('export_configuration.html', + available_columns=available_columns, + filters=filters, + has_location_accuracy_feature=has_location_accuracy) + + except Exception as e: + print(f"❌ Error in export_configuration route: {e}") + import traceback + print(f"❌ Traceback: {traceback.format_exc()}") + + # Use your existing logger error method with correct parameters + try: + logger_handler.log_flask_error( + error_type="export_configuration_error", + error_message=str(e), + stack_trace=traceback.format_exc() + ) + except Exception as log_error: + print(f"⚠️ Could not log error: {log_error}") + + flash('Error loading export configuration page.', 'error') + return redirect(url_for('attendance_report')) + +@app.route('/generate-excel-export', methods=['POST']) +@admin_required +def generate_excel_export(): + """Generate and download Excel file with selected columns""" + try: + print("📊 Excel export generation started") + + # Log export action using your existing logger + try: + logger_handler.logger.info(f"User {session.get('username', 'unknown')} generated Excel export") + except Exception as log_error: + print(f"⚠️ Logging error (non-critical): {log_error}") + + # Get selected columns and custom names from form + selected_columns = request.form.getlist('selected_columns') + print(f"📊 Selected columns: {selected_columns}") + + if not selected_columns: + flash('Please select at least one column to export.', 'error') + return redirect(url_for('export_configuration')) + + column_names = {} + for column in selected_columns: + column_names[column] = request.form.get(f'name_{column}', column) + + # Get filters + filters = { + 'date_from': request.form.get('date_from'), + 'date_to': request.form.get('date_to'), + 'location_filter': request.form.get('location_filter'), + 'employee_filter': request.form.get('employee_filter') + } + + print(f"📊 Export filters: {filters}") + + # Save user preferences in session for next time + session['export_preferences'] = { + 'selected_columns': selected_columns, + 'column_names': column_names + } + + # Generate Excel file + excel_file = create_excel_export(selected_columns, column_names, filters) + + if excel_file: + # Generate filename with timestamp + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f'attendance_report_{timestamp}.xlsx' + + print(f"📊 Excel file generated successfully: {filename}") + + # Log successful export using your existing logger + try: + logger_handler.logger.info(f"Excel export generated successfully with {len(selected_columns)} columns by user {session.get('username', 'unknown')}") + except Exception as log_error: + print(f"⚠️ Logging error (non-critical): {log_error}") + + return send_file( + excel_file, + as_attachment=True, + download_name=filename, + mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + else: + flash('Error generating Excel file.', 'error') + return redirect(url_for('export_configuration')) + + except Exception as e: + print(f"❌ Error in generate_excel_export route: {e}") + import traceback + print(f"❌ Traceback: {traceback.format_exc()}") + + # Use your existing logger error method with correct parameters + try: + logger_handler.log_flask_error( + error_type="excel_export_error", + error_message=str(e), + stack_trace=traceback.format_exc() + ) + except Exception as log_error: + print(f"⚠️ Could not log error: {log_error}") + + flash('Error generating Excel export.', 'error') + return redirect(url_for('export_configuration')) + +def create_excel_export(selected_columns, column_names, filters): + """Create Excel file with selected attendance data""" + try: + print(f"📊 Creating Excel export with {len(selected_columns)} columns") + + # Import openpyxl modules + try: + from openpyxl import Workbook + from openpyxl.styles import Font, Alignment, PatternFill + except ImportError as e: + print(f"❌ openpyxl import error: {e}") + print("💡 Install openpyxl: pip install openpyxl") + return None + + # Build query based on filters + query = db.session.query(AttendanceData).join(QRCode) + + # Apply date filters + if filters.get('date_from'): + try: + date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date() + query = query.filter(AttendanceData.check_in_date >= date_from) + print(f"📊 Applied date_from filter: {date_from}") + except ValueError as e: + print(f"⚠️ Invalid date_from format: {e}") + + if filters.get('date_to'): + try: + date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date() + query = query.filter(AttendanceData.check_in_date <= date_to) + print(f"📊 Applied date_to filter: {date_to}") + except ValueError as e: + print(f"⚠️ Invalid date_to format: {e}") + + # Apply location filter + if filters.get('location_filter'): + query = query.filter(AttendanceData.location_name.ilike(f"%{filters['location_filter']}%")) + print(f"📊 Applied location filter: {filters['location_filter']}") + + # Apply employee filter + if filters.get('employee_filter'): + query = query.filter(AttendanceData.employee_id.ilike(f"%{filters['employee_filter']}%")) + print(f"📊 Applied employee filter: {filters['employee_filter']}") + + # Execute query + records = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc()).all() + print(f"📊 Found {len(records)} records for export") + + if not records: + print("⚠️ No records found for export") + return None + + # Create workbook + wb = Workbook() + ws = wb.active + ws.title = "Attendance Report" + + # Define header style + header_font = Font(bold=True, color="FFFFFF") + header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") + header_alignment = Alignment(horizontal="center", vertical="center") + + # Add headers + for idx, column_key in enumerate(selected_columns, 1): + cell = ws.cell(row=1, column=idx) + cell.value = column_names.get(column_key, column_key) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + + # Add data + for row_idx, record in enumerate(records, 2): + for col_idx, column_key in enumerate(selected_columns, 1): + cell = ws.cell(row=row_idx, column=col_idx) + + # Get value based on column key + try: + if column_key == 'employee_id': + cell.value = record.employee_id + elif column_key == 'location_name': + cell.value = record.location_name + elif column_key == 'status': + cell.value = record.status + elif column_key == 'check_in_date': + cell.value = record.check_in_date.strftime('%Y-%m-%d') if record.check_in_date else '' + elif column_key == 'check_in_time': + cell.value = record.check_in_time.strftime('%H:%M:%S') if record.check_in_time else '' + elif column_key == 'qr_address': + cell.value = record.qr_code.location if record.qr_code else '' + elif column_key == 'address': + cell.value = record.address or '' + elif column_key == 'device_info': + cell.value = record.device_info or '' + elif column_key == 'ip_address': + cell.value = record.ip_address or '' + elif column_key == 'user_agent': + cell.value = record.user_agent or '' + elif column_key == 'latitude': + cell.value = record.latitude or '' + elif column_key == 'longitude': + cell.value = record.longitude or '' + elif column_key == 'accuracy': + cell.value = record.accuracy or '' + elif column_key == 'location_accuracy': + cell.value = record.location_accuracy or '' + else: + cell.value = '' + except Exception as cell_error: + print(f"⚠️ Error setting cell value for {column_key}: {cell_error}") + cell.value = '' + + # Auto-adjust column widths + for column in ws.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 50) + ws.column_dimensions[column_letter].width = adjusted_width + + # Save to BytesIO + excel_buffer = io.BytesIO() + wb.save(excel_buffer) + excel_buffer.seek(0) + + print("📊 Excel file created successfully") + return excel_buffer + + except Exception as e: + print(f"❌ Error creating Excel export: {e}") + import traceback + print(f"❌ Traceback: {traceback.format_exc()}") + return None + # Jinja2 filters for better template functionality @app.template_filter('days_since') def days_since_filter(date): diff --git a/static/css/export_configuration.css b/static/css/export_configuration.css new file mode 100644 index 0000000..5bb7793 --- /dev/null +++ b/static/css/export_configuration.css @@ -0,0 +1,449 @@ +/* Export Configuration Styles */ +.export-config-page { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 2rem 0; +} + +/* Header Section */ +.export-header { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 2rem; + margin: 0 auto 2rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.header-content h1 { + color: #2d3748; + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.header-content h1 i { + color: #48bb78; + font-size: 2.2rem; +} + +.header-content p { + color: #718096; + font-size: 1.1rem; + margin-bottom: 0; +} + +.header-actions { + display: flex; + gap: 1rem; + align-items: center; +} + +/* Applied Filters Section */ +.applied-filters-section { + max-width: 1200px; + margin: 0 auto 2rem; +} + +.filters-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 1.5rem; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +.filters-header { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.filters-header h3 { + color: #2d3748; + font-size: 1.4rem; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.filters-header i { + color: #4299e1; +} + +.filters-display { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.filter-tag { + background: linear-gradient(135deg, #4299e1, #3182ce); + color: white; + padding: 0.5rem 1rem; + border-radius: 25px; + font-size: 0.9rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + box-shadow: 0 2px 8px rgba(66, 153, 225, 0.3); +} + +/* Export Configuration Section */ +.export-config-section { + max-width: 1200px; + margin: 0 auto; +} + +.config-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.config-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; +} + +.config-header h3 { + color: #2d3748; + font-size: 1.6rem; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.config-header i { + color: #48bb78; +} + +.config-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +/* Columns Grid */ +.columns-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 1.5rem; +} + +.column-item { + background: linear-gradient(135deg, #f7fafc, #edf2f7); + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 1.5rem; + transition: all 0.3s ease; +} + +.column-item:hover { + border-color: #4299e1; + box-shadow: 0 4px 16px rgba(66, 153, 225, 0.15); + transform: translateY(-2px); +} + +.column-checkbox { + margin-bottom: 1rem; +} + +.column-checkbox input[type="checkbox"] { + display: none; +} + +.column-checkbox label { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + font-weight: 600; + color: #2d3748; + font-size: 1.1rem; + padding: 0.75rem; + border-radius: 8px; + transition: all 0.3s ease; +} + +.column-checkbox label i { + width: 20px; + height: 20px; + border: 2px solid #cbd5e0; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + color: transparent; + transition: all 0.3s ease; +} + +.column-checkbox input[type="checkbox"]:checked + label { + background: linear-gradient(135deg, #48bb78, #38a169); + color: white; +} + +.column-checkbox input[type="checkbox"]:checked + label i { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.5); + color: white; +} + +.column-name-input { + transition: all 0.3s ease; +} + +.column-name-input label { + display: block; + font-weight: 500; + color: #4a5568; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.column-name-input input { + width: 100%; + padding: 0.75rem; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1rem; + background: white; + transition: all 0.3s ease; +} + +.column-name-input input:focus { + outline: none; + border-color: #4299e1; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); +} + +/* Preview Section */ +.preview-section { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.preview-header h3 { + color: #2d3748; + font-size: 1.6rem; + font-weight: 600; + margin: 0 0 1.5rem 0; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.preview-header i { + color: #ed8936; +} + +.preview-table-container { + overflow-x: auto; + border-radius: 10px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +.preview-table { + width: 100%; + border-collapse: collapse; + background: white; + font-size: 0.9rem; +} + +.preview-table th { + background: linear-gradient(135deg, #4299e1, #3182ce); + color: white; + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 2px solid #2b6cb0; + white-space: nowrap; +} + +.preview-table td { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; + color: #4a5568; +} + +.preview-placeholder { + text-align: center; + padding: 3rem; + color: #a0aec0; + font-style: italic; + font-size: 1.1rem; +} + +/* Submit Section */ +.submit-section { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 2rem; + text-align: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.submit-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.submit-note { + color: #718096; + font-size: 0.95rem; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.submit-note i { + color: #4299e1; +} + +/* Button Styles */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all 0.3s ease; + font-size: 1rem; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.btn-success { + background: linear-gradient(135deg, #48bb78, #38a169); + color: white; +} + +.btn-success:hover { + background: linear-gradient(135deg, #38a169, #2f855a); +} + +.btn-secondary { + background: linear-gradient(135deg, #a0aec0, #718096); + color: white; +} + +.btn-secondary:hover { + background: linear-gradient(135deg, #718096, #4a5568); +} + +.btn-outline { + background: transparent; + color: #4299e1; + border: 2px solid #4299e1; +} + +.btn-outline:hover { + background: #4299e1; + color: white; +} + +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.btn-lg { + padding: 1rem 2rem; + font-size: 1.1rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .export-config-page { + padding: 1rem; + } + + .export-header { + flex-direction: column; + text-align: center; + } + + .header-content h1 { + font-size: 2rem; + } + + .columns-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .config-header { + flex-direction: column; + align-items: flex-start; + } + + .config-actions { + width: 100%; + justify-content: center; + } + + .config-card, + .preview-section, + .submit-section { + padding: 1.5rem; + } +} + +@media (max-width: 480px) { + .filters-display { + flex-direction: column; + } + + .config-actions { + flex-direction: column; + width: 100%; + } + + .btn { + width: 100%; + justify-content: center; + } + + .submit-actions { + width: 100%; + } +} \ No newline at end of file diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 201cb45..dbf7298 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -945,3 +945,129 @@ function exportAttendanceWithAccuracy() { link.click(); document.body.removeChild(link); } + +function exportAttendance() { + // Log export action + console.log('Export button clicked - redirecting to configuration page'); + + // Get current filters + const currentFilters = getCurrentFilters(); + + // Build URL with current filters + const params = new URLSearchParams(); + if (currentFilters.date_from) params.append('date_from', currentFilters.date_from); + if (currentFilters.date_to) params.append('date_to', currentFilters.date_to); + if (currentFilters.location) params.append('location', currentFilters.location); + if (currentFilters.employee) params.append('employee', currentFilters.employee); + + // Navigate to export configuration page + const configUrl = '/export-configuration' + (params.toString() ? '?' + params.toString() : ''); + window.location.href = configUrl; +} + +function getCurrentFilters() { + // Extract current filter values from the page + return { + date_from: document.getElementById('date_from')?.value || '', + date_to: document.getElementById('date_to')?.value || '', + location: document.getElementById('location')?.value || '', + employee: document.getElementById('employee')?.value || '' + }; +} + +// Add a quick CSV export function as backup (keep existing functionality) +function exportAttendanceCSV() { + // Build export URL with current filters for CSV + const params = new URLSearchParams(); + const filters = getCurrentFilters(); + + if (filters.date_from) params.append('date_from', filters.date_from); + if (filters.date_to) params.append('date_to', filters.date_to); + if (filters.location) params.append('location', filters.location); + if (filters.employee) params.append('employee', filters.employee); + params.append('export', 'csv'); + + // Create a temporary link and click it to download + const downloadUrl = window.location.pathname + '?' + params.toString(); + window.open(downloadUrl, '_blank'); +} + +// Enhanced export menu (if you want to add dropdown with multiple export options) +function showExportMenu() { + // Create export options menu + const existingMenu = document.getElementById('exportMenu'); + if (existingMenu) { + existingMenu.remove(); + return; + } + + const exportBtn = document.querySelector('button[onclick="exportAttendance()"]'); + if (!exportBtn) return; + + const menu = document.createElement('div'); + menu.id = 'exportMenu'; + menu.style.cssText = ` + position: absolute; + top: 100%; + right: 0; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + z-index: 1000; + min-width: 200px; + margin-top: 5px; + `; + + menu.innerHTML = ` +
+ + +
+ `; + + exportBtn.parentElement.style.position = 'relative'; + exportBtn.parentElement.appendChild(menu); + + // Close menu when clicking outside + setTimeout(() => { + document.addEventListener('click', function closeOnClickOutside(e) { + if (!menu.contains(e.target) && e.target !== exportBtn) { + closeExportMenu(); + document.removeEventListener('click', closeOnClickOutside); + } + }); + }, 100); +} + +function closeExportMenu() { + const menu = document.getElementById('exportMenu'); + if (menu) { + menu.remove(); + } +} + +// Initialize export functionality when page loads +document.addEventListener('DOMContentLoaded', function() { + // Update export button to use enhanced functionality + const exportBtn = document.querySelector('button[onclick="exportAttendance()"]'); + if (exportBtn) { + // You can modify the button to show a dropdown instead + // exportBtn.onclick = showExportMenu; + // exportBtn.innerHTML = ' Export Data '; + } + + console.log('Enhanced export functionality initialized'); +}); diff --git a/static/js/export_configuration.js b/static/js/export_configuration.js new file mode 100644 index 0000000..ce8fa20 --- /dev/null +++ b/static/js/export_configuration.js @@ -0,0 +1,160 @@ +document.addEventListener('DOMContentLoaded', function() { + console.log('Simple Export Configuration loaded'); + + // Setup basic event listeners + setupBasicListeners(); + + // Initial preview update + setTimeout(updatePreview, 100); +}); + +function setupBasicListeners() { + // Add listeners to checkboxes + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(function(checkbox) { + checkbox.addEventListener('change', function() { + toggleColumnName(this.value); + setTimeout(updatePreview, 50); + }); + }); + + // Add listeners to name inputs + const nameInputs = document.querySelectorAll('input[id^="name_"]'); + nameInputs.forEach(function(input) { + input.addEventListener('input', function() { + setTimeout(updatePreview, 300); + }); + }); + + // Form validation + const form = document.getElementById('exportForm'); + if (form) { + form.addEventListener('submit', function(e) { + const selected = document.querySelectorAll('input[name="selected_columns"]:checked'); + if (selected.length === 0) { + e.preventDefault(); + alert('Please select at least one column to export.'); + } + }); + } +} + +function toggleColumnName(columnKey) { + const checkbox = document.getElementById('col_' + columnKey); + const nameGroup = document.getElementById('name_group_' + columnKey); + + if (checkbox && nameGroup) { + if (checkbox.checked) { + nameGroup.style.display = 'block'; + } else { + nameGroup.style.display = 'none'; + } + } +} + +function updatePreview() { + const previewHeader = document.getElementById('previewHeader'); + const previewTable = document.querySelector('.preview-table tbody'); + + if (!previewHeader || !previewTable) return; + + const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked')); + + if (selectedColumns.length === 0) { + previewHeader.innerHTML = ''; + previewTable.innerHTML = 'No columns selected'; + updateGenerateButton(0); + return; + } + + // Build header + let headerHTML = ''; + selectedColumns.forEach(function(checkbox) { + const columnKey = checkbox.value; + const nameInput = document.getElementById('name_' + columnKey); + const customName = nameInput ? nameInput.value : columnKey; + headerHTML += '' + customName + ''; + }); + previewHeader.innerHTML = headerHTML; + + // Build sample row + let sampleRowHTML = ''; + selectedColumns.forEach(function(checkbox) { + const columnKey = checkbox.value; + const sampleData = getSampleData(columnKey); + sampleRowHTML += '' + sampleData + ''; + }); + sampleRowHTML += ''; + previewTable.innerHTML = sampleRowHTML; + + updateGenerateButton(selectedColumns.length); +} + +function getSampleData(columnKey) { + const samples = { + 'employee_id': 'EMP001', + 'location_name': 'Main Office', + 'status': 'Check In', + 'check_in_date': '2025-01-15', + 'check_in_time': '09:30:00', + 'qr_address': '123 Business St', + 'address': '123 Business St', + 'device_info': 'iPhone', + 'ip_address': '192.168.1.100', + 'user_agent': 'Mobile Safari', + 'latitude': '40.7128', + 'longitude': '-74.0060', + 'accuracy': '5.2', + 'location_accuracy': '0.003' + }; + return samples[columnKey] || 'Sample'; +} + +function updateGenerateButton(columnCount) { + const generateBtn = document.getElementById('generateBtn'); + if (!generateBtn) return; + + if (columnCount === 0) { + generateBtn.disabled = true; + generateBtn.innerHTML = 'Select columns to export'; + } else { + generateBtn.disabled = false; + generateBtn.innerHTML = 'Generate Excel Export (' + columnCount + ' columns)'; + } +} + +function selectAllColumns() { + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(function(checkbox) { + checkbox.checked = true; + toggleColumnName(checkbox.value); + }); + updatePreview(); +} + +function deselectAllColumns() { + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(function(checkbox) { + checkbox.checked = false; + toggleColumnName(checkbox.value); + }); + updatePreview(); +} + +function resetToDefaults() { + // Get checkboxes and reset to defaults + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(function(checkbox) { + // Default enabled columns + const defaultEnabled = ['employee_id', 'location_name', 'status', 'check_in_date', 'check_in_time']; + checkbox.checked = defaultEnabled.includes(checkbox.value); + toggleColumnName(checkbox.value); + + // Reset name input + const nameInput = document.getElementById('name_' + checkbox.value); + if (nameInput) { + nameInput.value = nameInput.getAttribute('value') || checkbox.value; + } + }); + updatePreview(); +} \ No newline at end of file diff --git a/templates/attendance_report.html b/templates/attendance_report.html index 999bde9..389f171 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -368,10 +368,6 @@ const userRole = '{{ session.role }}'; const hasEditPermission = ['admin', 'payroll'].includes(userRole); -// Debug logging -console.log('User Role:', userRole); -console.log('Has Edit Permission:', hasEditPermission); - // Enhanced JavaScript for new functionality const hasLocationAccuracy = {{ 'true' if has_location_accuracy_feature else 'false' }}; @@ -379,18 +375,6 @@ function clearFilters() { window.location.href = "{{ url_for('attendance_report') }}"; } -function exportAttendance() { - // Build export URL with current filters - const params = new URLSearchParams(); - if ('{{ date_from }}') params.append('date_from', '{{ date_from }}'); - if ('{{ date_to }}') params.append('date_to', '{{ date_to }}'); - if ('{{ location_filter }}') params.append('location', '{{ location_filter }}'); - if ('{{ employee_filter }}') params.append('employee', '{{ employee_filter }}'); - params.append('export', 'csv'); - - window.open(`{{ url_for('attendance_report') }}?${params.toString()}`); -} - function refreshReport() { window.location.reload(); } @@ -510,8 +494,6 @@ function viewRecordDetails(recordId) { // OVERRIDE EDIT/DELETE FUNCTIONS WITH PROPER PERMISSION CHECKING function editRecord(recordId) { console.log('Edit function called for record:', recordId); - console.log('User role:', userRole); - console.log('Has permission:', hasEditPermission); // Check permissions before allowing edit if (!hasEditPermission) { @@ -527,8 +509,6 @@ function editRecord(recordId) { function deleteRecord(recordId, employeeId) { console.log('Delete function called for record:', recordId); - console.log('User role:', userRole); - console.log('Has permission:', hasEditPermission); // Check permissions before allowing delete if (!hasEditPermission) { @@ -593,8 +573,7 @@ document.addEventListener('keydown', function(event) { // Force update buttons on page load for admin/payroll users document.addEventListener('DOMContentLoaded', function() { - console.log('Page loaded. User role:', userRole, 'Has edit permission:', hasEditPermission); - + if (hasEditPermission) { // Find all record action containers and update them const recordActions = document.querySelectorAll('.record-actions'); diff --git a/templates/export_configuration.html b/templates/export_configuration.html new file mode 100644 index 0000000..5b629e3 --- /dev/null +++ b/templates/export_configuration.html @@ -0,0 +1,261 @@ +{% extends "base_authenticated.html" %} + +{% block title %}Export Configuration - QR Code Management{% endblock %} + +{% block extra_head %} + + +{% endblock %} + +{% block content %} +
+ +
+
+

+ + Export Configuration +

+

Customize your attendance report export with selected columns and custom names

+
+ +
+ +
+
+ + + {% if filters.date_from or filters.date_to or filters.location_filter or filters.employee_filter %} +
+
+
+

+ + Applied Filters +

+
+
+ {% if filters.date_from %} + + + From: {{ filters.date_from }} + + {% endif %} + {% if filters.date_to %} + + + To: {{ filters.date_to }} + + {% endif %} + {% if filters.location_filter %} + + + Location: {{ filters.location_filter }} + + {% endif %} + {% if filters.employee_filter %} + + + Employee: {{ filters.employee_filter }} + + {% endif %} +
+
+
+ {% endif %} + + +
+
+ + + + + + +
+
+

+ + Select Columns to Export +

+
+ + + +
+
+ +
+ {% for column in available_columns %} +
+
+ + +
+ +
+ + +
+
+ {% endfor %} +
+
+ + +
+
+

+ + Export Preview +

+
+
+ + + + + + + + + + + +
+ Select columns above to see preview +
+
+
+ + +
+
+ +

+ + Your column preferences will be saved for next time +

+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + + +{% endblock %} \ No newline at end of file