diff --git a/app.py b/app.py index 06c9e41..9265d21 100644 --- a/app.py +++ b/app.py @@ -3518,7 +3518,7 @@ def export_configuration(): @app.route('/generate-excel-export', methods=['POST']) @admin_required def generate_excel_export(): - """Generate and download Excel file with selected columns""" + """Generate and download Excel file with selected columns in specified order""" try: print("📊 Excel export generation started") @@ -3529,8 +3529,31 @@ def generate_excel_export(): 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}") + selected_columns_raw = request.form.getlist('selected_columns') + print(f"📊 Selected columns (raw): {selected_columns_raw}") + + # Get column order from form + column_order_json = request.form.get('column_order', '[]') + try: + column_order = json.loads(column_order_json) if column_order_json else [] + except (json.JSONDecodeError, TypeError): + column_order = [] + + print(f"📊 Column order from form: {column_order}") + + # Determine final column order + if column_order: + # Use the specified order, but only include actually selected columns + selected_columns = [col for col in column_order if col in selected_columns_raw] + # Add any selected columns that weren't in the order (shouldn't happen, but safety check) + for col in selected_columns_raw: + if col not in selected_columns: + selected_columns.append(col) + else: + # Fallback to raw selection order + selected_columns = selected_columns_raw + + print(f"📊 Final column order: {selected_columns}") if not selected_columns: flash('Please select at least one column to export.', 'error') @@ -3549,15 +3572,17 @@ def generate_excel_export(): } print(f"📊 Export filters: {filters}") + print(f"📊 Column names: {column_names}") # Save user preferences in session for next time session['export_preferences'] = { 'selected_columns': selected_columns, - 'column_names': column_names + 'column_names': column_names, + 'column_order': selected_columns # This is now the ordered list } - # Generate Excel file - excel_file = create_excel_export(selected_columns, column_names, filters) + # Generate Excel file with ordered columns + excel_file = create_excel_export_ordered(selected_columns, column_names, filters) if excel_file: # Generate filename with timestamp @@ -3565,10 +3590,11 @@ def generate_excel_export(): filename = f'attendance_report_{timestamp}.xlsx' print(f"📊 Excel file generated successfully: {filename}") + print(f"📊 Column order in export: {selected_columns}") # 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')}") + logger_handler.logger.info(f"Excel export generated successfully with {len(selected_columns)} columns in custom order by user {session.get('username', 'unknown')}") except Exception as log_error: print(f"⚠️ Logging error (non-critical): {log_error}") @@ -3757,7 +3783,163 @@ def create_excel_export(selected_columns, column_names, filters): import traceback print(f"❌ Traceback: {traceback.format_exc()}") return None - + +def create_excel_export_ordered(selected_columns, column_names, filters): + """Create Excel file with selected attendance data in specified column order""" + try: + print(f"📊 Creating Excel export with {len(selected_columns)} columns in order: {selected_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 - JOIN with QRCode to get location_event and location_address + query = db.session.query(AttendanceData, QRCode).join(QRCode, AttendanceData.qr_code_id == QRCode.id) + + # 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 and get results + results = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc()).all() + print(f"📊 Found {len(results)} records for export") + + if not results: + 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 in the specified order + 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 + print(f"📊 Column {idx}: {column_key} -> '{cell.value}'") + + # Add data in the same column order + for row_idx, (attendance_record, qr_code) in enumerate(results, 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 (same logic as before) + try: + if column_key == 'employee_id': + cell.value = attendance_record.employee_id + elif column_key == 'location_name': + cell.value = attendance_record.location_name + elif column_key == 'status': + # Use location_event from QR code instead of status + cell.value = qr_code.location_event if qr_code.location_event else 'Check In' + elif column_key == 'check_in_date': + cell.value = attendance_record.check_in_date.strftime('%Y-%m-%d') if attendance_record.check_in_date else '' + elif column_key == 'check_in_time': + cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else '' + elif column_key == 'qr_address': + # Use QR Code address (location_address), not location + cell.value = qr_code.location_address if qr_code and qr_code.location_address else '' + elif column_key == 'address': + # Check-in address logic based on location accuracy + if hasattr(attendance_record, 'location_accuracy') and attendance_record.location_accuracy is not None: + try: + accuracy_value = float(attendance_record.location_accuracy) + if accuracy_value <= 0.5: + # High accuracy - use QR code ADDRESS (not location) + cell.value = qr_code.location_address if qr_code and qr_code.location_address else '' + else: + # Lower accuracy - use actual check-in address + cell.value = attendance_record.address or '' + except (ValueError, TypeError): + # If accuracy can't be converted to float, use check-in address + cell.value = attendance_record.address or '' + else: + # No location accuracy data - use actual check-in address + cell.value = attendance_record.address or '' + elif column_key == 'device_info': + cell.value = attendance_record.device_info or '' + elif column_key == 'ip_address': + cell.value = attendance_record.ip_address or '' + elif column_key == 'user_agent': + cell.value = attendance_record.user_agent or '' + elif column_key == 'latitude': + cell.value = attendance_record.latitude or '' + elif column_key == 'longitude': + cell.value = attendance_record.longitude or '' + elif column_key == 'accuracy': + cell.value = attendance_record.accuracy or '' + elif column_key == 'location_accuracy': + cell.value = attendance_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 with custom column order") + 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 index 5bb7793..2b24266 100644 --- a/static/css/export_configuration.css +++ b/static/css/export_configuration.css @@ -446,4 +446,301 @@ .submit-actions { width: 100%; } +} + +/* Instructions Card */ +.instructions-card { + background: linear-gradient(135deg, #e3f2fd, #f3e5f5); + border: 2px solid #4fc3f7; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 4px 12px rgba(79, 195, 247, 0.15); +} + +.instructions-content h4 { + color: #1565c0; + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.instructions-content ul { + list-style: none; + padding: 0; + margin: 0; +} + +.instructions-content li { + padding: 0.5rem 0; + color: #424242; + font-size: 0.95rem; + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.instructions-content li::before { + content: "✓"; + color: #4caf50; + font-weight: bold; + font-size: 1.1rem; + margin-top: 0.1rem; +} + +.instructions-content .highlight { + background: linear-gradient(135deg, #ffeb3b, #ffc107); + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-weight: 600; + color: #f57c00; +} + +/* Columns Section */ +.columns-section { + margin-bottom: 2rem; +} + +.columns-section h4 { + color: #2d3748; + font-size: 1.4rem; + font-weight: 600; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +/* Selected Columns Section */ +.selected-columns-section { + background: linear-gradient(135deg, #f8f9fa, #e9ecef); + border: 2px dashed #6c757d; + border-radius: 12px; + padding: 2rem; + margin-top: 2rem; + transition: all 0.3s ease; +} + +.selected-columns-section.has-columns { + border-color: #28a745; + border-style: solid; + background: linear-gradient(135deg, #f8fff8, #e8f5e8); +} + +.selected-columns-section h4 { + color: #495057; + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.drag-hint { + font-size: 0.85rem; + color: #6c757d; + font-weight: normal; + font-style: italic; +} + +/* Selected Columns List */ +.selected-columns-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-height: 60px; +} + +.selected-column-item { + background: white; + border: 2px solid #e2e8f0; + border-radius: 8px; + padding: 1rem; + cursor: move; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.selected-column-item:hover { + border-color: #4299e1; + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(66, 153, 225, 0.15); +} + +.selected-column-item.sortable-drag { + opacity: 0.8; + transform: rotate(5deg); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + z-index: 1000; +} + +.selected-column-item.sortable-ghost { + opacity: 0.4; + background: #f1f5f9; +} + +.selected-column-item.sortable-chosen { + border-color: #3b82f6; + background: #eff6ff; +} + +.selected-column-info { + display: flex; + align-items: center; + gap: 1rem; + flex: 1; +} + +.column-drag-handle { + color: #9ca3af; + font-size: 1.2rem; + cursor: grab; + padding: 0.5rem; + border-radius: 4px; + transition: all 0.2s ease; +} + +.column-drag-handle:hover { + color: #4b5563; + background: #f3f4f6; +} + +.column-drag-handle:active { + cursor: grabbing; +} + +.selected-column-details { + flex: 1; +} + +.selected-column-name { + font-weight: 600; + color: #1f2937; + font-size: 1rem; + margin-bottom: 0.25rem; +} + +.selected-column-export-name { + font-size: 0.85rem; + color: #6b7280; + font-style: italic; +} + +.column-order-number { + background: linear-gradient(135deg, #4f46e5, #7c3aed); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.9rem; + box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3); +} + +/* Empty state for selected columns */ +.selected-columns-empty { + text-align: center; + padding: 2rem; + color: #6b7280; + font-style: italic; +} + +.selected-columns-empty i { + font-size: 2rem; + margin-bottom: 1rem; + color: #9ca3af; +} + +/* Enhanced column items in grid */ +.column-item.selected { + border-color: #10b981; + background: linear-gradient(135deg, #f0fdf4, #ecfdf5); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(16, 185, 129, 0.15); +} + +.column-item.selected .column-checkbox label { + background: linear-gradient(135deg, #10b981, #059669); + color: white; +} + +/* Responsive design for drag & drop */ +@media (max-width: 768px) { + .selected-columns-section { + padding: 1rem; + } + + .selected-column-item { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .selected-column-info { + width: 100%; + flex-direction: column; + align-items: flex-start; + } + + .column-order-number { + align-self: flex-end; + } + + .drag-hint { + display: none; + } +} + +/* Animation for smooth transitions */ +.selected-columns-list .selected-column-item { + animation: slideInUp 0.3s ease-out; +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Sortable.js additional styles */ +.sortable-fallback { + display: none; +} + +/* Enhanced preview table to show column order */ +.preview-table thead th { + position: relative; +} + +.preview-table thead th::before { + content: attr(data-order); + position: absolute; + top: -8px; + right: -8px; + background: #4f46e5; + color: white; + font-size: 0.7rem; + font-weight: 600; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } \ No newline at end of file diff --git a/static/js/export_configuration.js b/static/js/export_configuration.js index 5c552ba..df38f6b 100644 --- a/static/js/export_configuration.js +++ b/static/js/export_configuration.js @@ -1,54 +1,264 @@ +/** + * Enhanced Export Configuration JavaScript with Drag & Drop + * Handles column selection, preview updates, drag & drop reordering, and preference management + */ + +// Global variables +let availableColumns = []; +let sortableInstance = null; +let savedColumnOrder = []; + +// Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', function() { - console.log('Simple Export Configuration loaded'); + console.log('Enhanced Export Configuration with Drag & Drop initialized'); - // Setup basic event listeners - setupBasicListeners(); + // Initialize available columns data + initializeColumnsData(); - // Initial preview update - setTimeout(updatePreview, 100); + // Set up event listeners + setupEventListeners(); + + // Load saved preferences if available + loadSavedPreferences(); + + // Update preview on load + updatePreview(); + + // Initialize drag & drop + initializeDragDrop(); }); -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); +function initializeColumnsData() { + try { + // Extract column data from the form + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + availableColumns = Array.from(checkboxes).map(cb => { + const columnKey = cb.value; + const label = cb.parentElement.querySelector('label').textContent.trim(); + const nameInput = document.getElementById('name_' + columnKey); + + return { + key: columnKey, + label: label, + defaultName: nameInput ? nameInput.value : label, + enabled: cb.checked + }; }); - }); - - // Add listeners to name inputs - const nameInputs = document.querySelectorAll('input[id^="name_"]'); - nameInputs.forEach(function(input) { - input.addEventListener('input', function() { - setTimeout(updatePreview, 300); + + console.log('Initialized columns data:', availableColumns); + } catch (error) { + console.error('Error initializing columns data:', error); + } +} + +function setupEventListeners() { + try { + // Add change listeners to all column checkboxes + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(checkbox => { + checkbox.addEventListener('change', function() { + toggleColumnName(this.value); + updateSelectedColumnsList(); + updatePreview(); + + // Add visual feedback + const columnItem = this.closest('.column-item'); + if (this.checked) { + columnItem.classList.add('selected'); + } else { + columnItem.classList.remove('selected'); + } + }); }); - }); - - // 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.'); - } + + // Add input listeners to all column name inputs + const nameInputs = document.querySelectorAll('input[id^="name_"]'); + nameInputs.forEach(input => { + input.addEventListener('input', debounce(() => { + updateSelectedColumnsList(); + updatePreview(); + }, 300)); }); + + // Form validation before submit + const form = document.getElementById('exportForm'); + if (form) { + form.addEventListener('submit', function(e) { + if (!validateForm()) { + e.preventDefault(); + } + }); + } + } catch (error) { + console.error('Error setting up event listeners:', error); } } 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'; + try { + const checkbox = document.getElementById('col_' + columnKey); + const nameGroup = document.getElementById('name_group_' + columnKey); + + if (checkbox && nameGroup) { + if (checkbox.checked) { + nameGroup.style.display = 'block'; + nameGroup.style.opacity = '0'; + setTimeout(() => { + nameGroup.style.opacity = '1'; + }, 10); + } else { + nameGroup.style.opacity = '0'; + setTimeout(() => { + nameGroup.style.display = 'none'; + }, 300); + } } + } catch (error) { + console.error('Error toggling column name:', error); + } +} + +function initializeDragDrop() { + try { + const selectedColumnsList = document.getElementById('selectedColumnsList'); + if (selectedColumnsList) { + sortableInstance = Sortable.create(selectedColumnsList, { + animation: 200, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + handle: '.column-drag-handle', + onStart: function(evt) { + console.log('Drag started:', evt.oldIndex); + }, + onEnd: function(evt) { + console.log('Drag ended:', evt.oldIndex, '->', evt.newIndex); + updateColumnOrderNumbers(); + updatePreview(); + + // Save the new order + savePreferences(); + } + }); + + console.log('Drag & drop initialized successfully'); + } + } catch (error) { + console.error('Error initializing drag & drop:', error); + } +} + +function updateSelectedColumnsList() { + try { + const selectedColumnsList = document.getElementById('selectedColumnsList'); + const selectedColumnsSection = document.getElementById('selectedColumnsSection'); + + if (!selectedColumnsList || !selectedColumnsSection) return; + + // Get currently selected columns + const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked')); + + if (selectedColumns.length === 0) { + selectedColumnsSection.style.display = 'none'; + return; + } + + selectedColumnsSection.style.display = 'block'; + selectedColumnsSection.classList.add('has-columns'); + + // Get current order if exists, otherwise use selection order + let orderedColumns = []; + if (savedColumnOrder.length > 0) { + // Use saved order, but only include currently selected columns + orderedColumns = savedColumnOrder.filter(key => + selectedColumns.some(cb => cb.value === key) + ); + // Add any newly selected columns that weren't in saved order + selectedColumns.forEach(cb => { + if (!orderedColumns.includes(cb.value)) { + orderedColumns.push(cb.value); + } + }); + } else { + orderedColumns = selectedColumns.map(cb => cb.value); + } + + // Build the selected columns list HTML + let listHTML = ''; + orderedColumns.forEach((columnKey, index) => { + const nameInput = document.getElementById('name_' + columnKey); + const columnData = availableColumns.find(col => col.key === columnKey); + const customName = nameInput ? nameInput.value : (columnData ? columnData.label : columnKey); + + listHTML += ` +
+
+
+ +
+
+
${columnData ? columnData.label : columnKey}
+
Export as: "${customName}"
+
+
+
${index + 1}
+
+ `; + }); + + if (listHTML === '') { + listHTML = ` +
+ +

Select columns above to see them here for reordering

+
+ `; + } + + selectedColumnsList.innerHTML = listHTML; + + // Re-initialize sortable after updating content + if (sortableInstance) { + sortableInstance.destroy(); + } + initializeDragDrop(); + + console.log(`Updated selected columns list with ${orderedColumns.length} columns`); + } catch (error) { + console.error('Error updating selected columns list:', error); + } +} + +function updateColumnOrderNumbers() { + try { + const orderNumbers = document.querySelectorAll('.column-order-number'); + orderNumbers.forEach((element, index) => { + element.textContent = index + 1; + }); + } catch (error) { + console.error('Error updating column order numbers:', error); + } +} + +function getCurrentColumnOrder() { + try { + const selectedItems = document.querySelectorAll('.selected-column-item'); + return Array.from(selectedItems).map(item => item.dataset.columnKey); + } catch (error) { + console.error('Error getting current column order:', error); + return []; + } +} + +function updateColumnOrderField() { + try { + const columnOrderField = document.getElementById('column_order'); + const currentOrder = getCurrentColumnOrder(); + if (columnOrderField) { + columnOrderField.value = JSON.stringify(currentOrder); + } + } catch (error) { + console.error('Error updating column order field:', error); } } @@ -62,10 +272,10 @@ function updatePreview() { return; } - // Get selected columns - const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked')); + // Get selected columns in their current order + const currentOrder = getCurrentColumnOrder(); - if (selectedColumns.length === 0) { + if (currentOrder.length === 0) { // No columns selected previewHeader.innerHTML = ''; previewTable.innerHTML = ` @@ -80,21 +290,19 @@ function updatePreview() { return; } - // Build header + // Build header with order numbers let headerHTML = ''; - selectedColumns.forEach(checkbox => { - const columnKey = checkbox.value; + currentOrder.forEach((columnKey, index) => { const nameInput = document.getElementById('name_' + columnKey); const customName = nameInput ? nameInput.value : columnKey; - headerHTML += `${customName}`; + headerHTML += `${customName}`; }); previewHeader.innerHTML = headerHTML; - // Build sample data row with enhanced address logic preview + // Build sample data row let sampleRowHTML = ''; - selectedColumns.forEach(checkbox => { - const columnKey = checkbox.value; + currentOrder.forEach(columnKey => { let sampleData = getSampleData(columnKey); // Special handling for address column to show the logic @@ -107,11 +315,11 @@ function updatePreview() { sampleRowHTML += ''; // Add explanation row if address column is selected - const hasAddressColumn = selectedColumns.some(cb => cb.value === 'address'); + const hasAddressColumn = currentOrder.includes('address'); if (hasAddressColumn) { sampleRowHTML += ` - + * Check-in Address: Shows QR address when location accuracy ≤ 0.5 miles, otherwise shows actual GPS address @@ -122,20 +330,20 @@ function updatePreview() { previewTable.innerHTML = sampleRowHTML; // Update generate button - updateGenerateButton(selectedColumns.length); + updateGenerateButton(currentOrder.length); - console.log(`Preview updated with ${selectedColumns.length} columns`); + console.log(`Preview updated with ${currentOrder.length} columns in order:`, currentOrder); } catch (error) { console.error('Error updating preview:', error); } } function getSampleData(columnKey) { - // Return sample data for each column type - Updated with correct event types + // Return sample data for each column type const sampleData = { 'employee_id': 'EMP001', 'location_name': 'Main Office', - 'status': 'Check In', // This will show "Check In" or "Check Out" from QR code + 'status': 'Check In', 'check_in_date': '2025-08-14', 'check_in_time': '09:30:00', 'qr_address': '123 Business St, City', @@ -152,51 +360,277 @@ function getSampleData(columnKey) { return sampleData[columnKey] || 'Sample Data'; } -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() { + try { + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(checkbox => { + if (!checkbox.checked) { + checkbox.checked = true; + checkbox.closest('.column-item').classList.add('selected'); + toggleColumnName(checkbox.value); + } + }); + updateSelectedColumnsList(); + updatePreview(); + + console.log('All columns selected'); + } catch (error) { + console.error('Error selecting all columns:', error); } } -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(); + try { + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(checkbox => { + if (checkbox.checked) { + checkbox.checked = false; + checkbox.closest('.column-item').classList.remove('selected'); + toggleColumnName(checkbox.value); + } + }); + updateSelectedColumnsList(); + updatePreview(); + + console.log('All columns deselected'); + } catch (error) { + console.error('Error deselecting all columns:', error); + } } 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); + try { + // Reset to default selections + availableColumns.forEach(column => { + const checkbox = document.getElementById('col_' + column.key); + const nameInput = document.getElementById('name_' + column.key); + const columnItem = checkbox ? checkbox.closest('.column-item') : null; + + if (checkbox) { + checkbox.checked = column.enabled; + if (column.enabled) { + columnItem?.classList.add('selected'); + } else { + columnItem?.classList.remove('selected'); + } + toggleColumnName(column.key); + } + + if (nameInput) { + nameInput.value = column.defaultName; + } + }); - // Reset name input - const nameInput = document.getElementById('name_' + checkbox.value); - if (nameInput) { - nameInput.value = nameInput.getAttribute('value') || checkbox.value; + // Clear saved order + savedColumnOrder = []; + + updateSelectedColumnsList(); + updatePreview(); + + // Clear saved preferences + try { + localStorage.removeItem('exportPreferences'); + } catch (storageError) { + console.warn('Could not clear saved preferences:', storageError); } - }); - updatePreview(); -} \ No newline at end of file + + console.log('Reset to default settings'); + } catch (error) { + console.error('Error resetting to defaults:', error); + } +} + +function updateGenerateButton(columnCount) { + try { + const generateBtn = document.getElementById('generateBtn'); + if (!generateBtn) return; + + if (columnCount === 0) { + generateBtn.disabled = true; + generateBtn.innerHTML = ' Select columns to export'; + generateBtn.classList.add('btn-disabled'); + } else { + generateBtn.disabled = false; + generateBtn.innerHTML = ` Generate Excel Export (${columnCount} columns)`; + generateBtn.classList.remove('btn-disabled'); + } + } catch (error) { + console.error('Error updating generate button:', error); + } +} + +function validateForm() { + try { + const selectedColumns = document.querySelectorAll('input[name="selected_columns"]:checked'); + + if (selectedColumns.length === 0) { + alert('Please select at least one column to export.'); + return false; + } + + // Validate that all selected columns have names + let hasEmptyNames = false; + selectedColumns.forEach(checkbox => { + const nameInput = document.getElementById('name_' + checkbox.value); + if (nameInput && nameInput.value.trim() === '') { + hasEmptyNames = true; + nameInput.style.borderColor = '#e53e3e'; + nameInput.focus(); + } else if (nameInput) { + nameInput.style.borderColor = '#e2e8f0'; + } + }); + + if (hasEmptyNames) { + alert('Please provide names for all selected columns.'); + return false; + } + + // Update column order field before submitting + updateColumnOrderField(); + + // Save preferences before submitting + savePreferences(); + + return true; + } catch (error) { + console.error('Error validating form:', error); + return false; + } +} + +function savePreferences() { + try { + const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked')) + .map(cb => cb.value); + + const columnNames = {}; + selectedColumns.forEach(col => { + const input = document.getElementById('name_' + col); + if (input) { + columnNames[col] = input.value.trim(); + } + }); + + // Get current column order + const columnOrder = getCurrentColumnOrder(); + + const prefs = { + selected_columns: selectedColumns, + column_names: columnNames, + column_order: columnOrder, + timestamp: new Date().toISOString() + }; + + localStorage.setItem('exportPreferences', JSON.stringify(prefs)); + console.log('Preferences saved with column order:', prefs); + + } catch (e) { + console.warn('Could not save preferences:', e); + } +} + +function loadSavedPreferences() { + try { + const savedPrefs = localStorage.getItem('exportPreferences'); + if (!savedPrefs) { + console.log('No saved preferences found'); + return; + } + + const prefs = JSON.parse(savedPrefs); + + // Check if preferences are not too old (30 days) + const savedDate = new Date(prefs.timestamp || 0); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + + if (savedDate < thirtyDaysAgo) { + localStorage.removeItem('exportPreferences'); + console.log('Saved preferences are too old, removed'); + return; + } + + // Apply saved column selections + if (prefs.selected_columns) { + const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); + checkboxes.forEach(cb => { + const shouldBeChecked = prefs.selected_columns.includes(cb.value); + if (cb.checked !== shouldBeChecked) { + cb.checked = shouldBeChecked; + const columnItem = cb.closest('.column-item'); + if (shouldBeChecked) { + columnItem?.classList.add('selected'); + } else { + columnItem?.classList.remove('selected'); + } + toggleColumnName(cb.value); + } + }); + } + + // Apply saved column names + if (prefs.column_names) { + Object.keys(prefs.column_names).forEach(key => { + const input = document.getElementById('name_' + key); + if (input && prefs.column_names[key]) { + input.value = prefs.column_names[key]; + } + }); + } + + // Save column order for later use + if (prefs.column_order) { + savedColumnOrder = prefs.column_order; + } + + console.log('Preferences loaded:', prefs); + + } catch (e) { + console.warn('Could not load saved preferences:', e); + try { + localStorage.removeItem('exportPreferences'); + } catch (removeError) { + console.warn('Could not remove invalid preferences:', removeError); + } + } +} + +// Utility function for debouncing +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Global functions for template usage +window.selectAllColumns = selectAllColumns; +window.deselectAllColumns = deselectAllColumns; +window.resetToDefaults = resetToDefaults; +window.toggleColumnName = toggleColumnName; +window.updateSelectedColumnsList = updateSelectedColumnsList; +window.getCurrentColumnOrder = getCurrentColumnOrder; +window.updateColumnOrderField = updateColumnOrderField; +window.savePreferences = savePreferences; + +// Add CSS for disabled button +const style = document.createElement('style'); +style.textContent = ` +.btn-disabled { + opacity: 0.6 !important; + cursor: not-allowed !important; + background: #a0aec0 !important; + pointer-events: none; +} + +.btn-disabled:hover { + transform: none !important; + box-shadow: none !important; +} +`; +document.head.appendChild(style); \ No newline at end of file diff --git a/templates/export_configuration.html b/templates/export_configuration.html index 5b629e3..60c5a79 100644 --- a/templates/export_configuration.html +++ b/templates/export_configuration.html @@ -5,6 +5,8 @@ {% block extra_head %} + + {% endblock %} {% block content %} @@ -16,7 +18,7 @@ Export Configuration -

Customize your attendance report export with selected columns and custom names

+

Customize your attendance report export with selected columns, custom names, and column ordering

@@ -76,52 +78,83 @@ + + +

- Select Columns to Export + Select & Order Columns for Export

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

How to Use

+
    +
  • Select columns: Check the boxes for columns you want to export
  • +
  • Customize names: Edit the "Export as" field to change column headers
  • +
  • Reorder columns: Drag & drop the selected columns to change their order in the Excel file
  • +
  • Preview: See how your export will look in the preview below
  • +
+
+
+ + +
+

Available Columns

+
+ {% for column in available_columns %} +
+
+ + +
+ +
+ + +
+
+ {% endfor %} +
+
+ + +
@@ -160,7 +193,7 @@

- Your column preferences will be saved for next time + Your column preferences and order will be saved for next time

@@ -174,13 +207,16 @@