Update report functions
This commit is contained in:
@@ -3518,7 +3518,7 @@ def export_configuration():
|
|||||||
@app.route('/generate-excel-export', methods=['POST'])
|
@app.route('/generate-excel-export', methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def generate_excel_export():
|
def generate_excel_export():
|
||||||
"""Generate and download Excel file with selected columns"""
|
"""Generate and download Excel file with selected columns in specified order"""
|
||||||
try:
|
try:
|
||||||
print("📊 Excel export generation started")
|
print("📊 Excel export generation started")
|
||||||
|
|
||||||
@@ -3529,8 +3529,31 @@ def generate_excel_export():
|
|||||||
print(f"⚠️ Logging error (non-critical): {log_error}")
|
print(f"⚠️ Logging error (non-critical): {log_error}")
|
||||||
|
|
||||||
# Get selected columns and custom names from form
|
# Get selected columns and custom names from form
|
||||||
selected_columns = request.form.getlist('selected_columns')
|
selected_columns_raw = request.form.getlist('selected_columns')
|
||||||
print(f"📊 Selected columns: {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:
|
if not selected_columns:
|
||||||
flash('Please select at least one column to export.', 'error')
|
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"📊 Export filters: {filters}")
|
||||||
|
print(f"📊 Column names: {column_names}")
|
||||||
|
|
||||||
# Save user preferences in session for next time
|
# Save user preferences in session for next time
|
||||||
session['export_preferences'] = {
|
session['export_preferences'] = {
|
||||||
'selected_columns': selected_columns,
|
'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
|
# Generate Excel file with ordered columns
|
||||||
excel_file = create_excel_export(selected_columns, column_names, filters)
|
excel_file = create_excel_export_ordered(selected_columns, column_names, filters)
|
||||||
|
|
||||||
if excel_file:
|
if excel_file:
|
||||||
# Generate filename with timestamp
|
# Generate filename with timestamp
|
||||||
@@ -3565,10 +3590,11 @@ def generate_excel_export():
|
|||||||
filename = f'attendance_report_{timestamp}.xlsx'
|
filename = f'attendance_report_{timestamp}.xlsx'
|
||||||
|
|
||||||
print(f"📊 Excel file generated successfully: {filename}")
|
print(f"📊 Excel file generated successfully: {filename}")
|
||||||
|
print(f"📊 Column order in export: {selected_columns}")
|
||||||
|
|
||||||
# Log successful export using your existing logger
|
# Log successful export using your existing logger
|
||||||
try:
|
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:
|
except Exception as log_error:
|
||||||
print(f"⚠️ Logging error (non-critical): {log_error}")
|
print(f"⚠️ Logging error (non-critical): {log_error}")
|
||||||
|
|
||||||
@@ -3758,6 +3784,162 @@ def create_excel_export(selected_columns, column_names, filters):
|
|||||||
print(f"❌ Traceback: {traceback.format_exc()}")
|
print(f"❌ Traceback: {traceback.format_exc()}")
|
||||||
return None
|
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
|
# Jinja2 filters for better template functionality
|
||||||
@app.template_filter('days_since')
|
@app.template_filter('days_since')
|
||||||
def days_since_filter(date):
|
def days_since_filter(date):
|
||||||
|
|||||||
@@ -447,3 +447,300 @@
|
|||||||
width: 100%;
|
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);
|
||||||
|
}
|
||||||
@@ -1,55 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* 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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
console.log('Simple Export Configuration loaded');
|
console.log('Enhanced Export Configuration with Drag & Drop initialized');
|
||||||
|
|
||||||
// Setup basic event listeners
|
// Initialize available columns data
|
||||||
setupBasicListeners();
|
initializeColumnsData();
|
||||||
|
|
||||||
// Initial preview update
|
// Set up event listeners
|
||||||
setTimeout(updatePreview, 100);
|
setupEventListeners();
|
||||||
|
|
||||||
|
// Load saved preferences if available
|
||||||
|
loadSavedPreferences();
|
||||||
|
|
||||||
|
// Update preview on load
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
// Initialize drag & drop
|
||||||
|
initializeDragDrop();
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupBasicListeners() {
|
function initializeColumnsData() {
|
||||||
// Add listeners to checkboxes
|
try {
|
||||||
|
// Extract column data from the form
|
||||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||||
checkboxes.forEach(function(checkbox) {
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
checkbox.addEventListener('change', function() {
|
||||||
toggleColumnName(this.value);
|
toggleColumnName(this.value);
|
||||||
setTimeout(updatePreview, 50);
|
updateSelectedColumnsList();
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
// Add visual feedback
|
||||||
|
const columnItem = this.closest('.column-item');
|
||||||
|
if (this.checked) {
|
||||||
|
columnItem.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
columnItem.classList.remove('selected');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add listeners to name inputs
|
// Add input listeners to all column name inputs
|
||||||
const nameInputs = document.querySelectorAll('input[id^="name_"]');
|
const nameInputs = document.querySelectorAll('input[id^="name_"]');
|
||||||
nameInputs.forEach(function(input) {
|
nameInputs.forEach(input => {
|
||||||
input.addEventListener('input', function() {
|
input.addEventListener('input', debounce(() => {
|
||||||
setTimeout(updatePreview, 300);
|
updateSelectedColumnsList();
|
||||||
});
|
updatePreview();
|
||||||
|
}, 300));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form validation
|
// Form validation before submit
|
||||||
const form = document.getElementById('exportForm');
|
const form = document.getElementById('exportForm');
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
const selected = document.querySelectorAll('input[name="selected_columns"]:checked');
|
if (!validateForm()) {
|
||||||
if (selected.length === 0) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Please select at least one column to export.');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting up event listeners:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleColumnName(columnKey) {
|
function toggleColumnName(columnKey) {
|
||||||
|
try {
|
||||||
const checkbox = document.getElementById('col_' + columnKey);
|
const checkbox = document.getElementById('col_' + columnKey);
|
||||||
const nameGroup = document.getElementById('name_group_' + columnKey);
|
const nameGroup = document.getElementById('name_group_' + columnKey);
|
||||||
|
|
||||||
if (checkbox && nameGroup) {
|
if (checkbox && nameGroup) {
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
nameGroup.style.display = 'block';
|
nameGroup.style.display = 'block';
|
||||||
|
nameGroup.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
nameGroup.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
} else {
|
} else {
|
||||||
|
nameGroup.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
nameGroup.style.display = 'none';
|
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 += `
|
||||||
|
<div class="selected-column-item" data-column-key="${columnKey}">
|
||||||
|
<div class="selected-column-info">
|
||||||
|
<div class="column-drag-handle" title="Drag to reorder">
|
||||||
|
<i class="fas fa-grip-vertical"></i>
|
||||||
|
</div>
|
||||||
|
<div class="selected-column-details">
|
||||||
|
<div class="selected-column-name">${columnData ? columnData.label : columnKey}</div>
|
||||||
|
<div class="selected-column-export-name">Export as: "${customName}"</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column-order-number">${index + 1}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listHTML === '') {
|
||||||
|
listHTML = `
|
||||||
|
<div class="selected-columns-empty">
|
||||||
|
<i class="fas fa-hand-point-up"></i>
|
||||||
|
<p>Select columns above to see them here for reordering</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreview() {
|
function updatePreview() {
|
||||||
@@ -62,10 +272,10 @@ function updatePreview() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selected columns
|
// Get selected columns in their current order
|
||||||
const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked'));
|
const currentOrder = getCurrentColumnOrder();
|
||||||
|
|
||||||
if (selectedColumns.length === 0) {
|
if (currentOrder.length === 0) {
|
||||||
// No columns selected
|
// No columns selected
|
||||||
previewHeader.innerHTML = '';
|
previewHeader.innerHTML = '';
|
||||||
previewTable.innerHTML = `
|
previewTable.innerHTML = `
|
||||||
@@ -80,21 +290,19 @@ function updatePreview() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build header
|
// Build header with order numbers
|
||||||
let headerHTML = '';
|
let headerHTML = '';
|
||||||
selectedColumns.forEach(checkbox => {
|
currentOrder.forEach((columnKey, index) => {
|
||||||
const columnKey = checkbox.value;
|
|
||||||
const nameInput = document.getElementById('name_' + columnKey);
|
const nameInput = document.getElementById('name_' + columnKey);
|
||||||
const customName = nameInput ? nameInput.value : columnKey;
|
const customName = nameInput ? nameInput.value : columnKey;
|
||||||
|
|
||||||
headerHTML += `<th>${customName}</th>`;
|
headerHTML += `<th data-order="${index + 1}">${customName}</th>`;
|
||||||
});
|
});
|
||||||
previewHeader.innerHTML = headerHTML;
|
previewHeader.innerHTML = headerHTML;
|
||||||
|
|
||||||
// Build sample data row with enhanced address logic preview
|
// Build sample data row
|
||||||
let sampleRowHTML = '<tr>';
|
let sampleRowHTML = '<tr>';
|
||||||
selectedColumns.forEach(checkbox => {
|
currentOrder.forEach(columnKey => {
|
||||||
const columnKey = checkbox.value;
|
|
||||||
let sampleData = getSampleData(columnKey);
|
let sampleData = getSampleData(columnKey);
|
||||||
|
|
||||||
// Special handling for address column to show the logic
|
// Special handling for address column to show the logic
|
||||||
@@ -107,11 +315,11 @@ function updatePreview() {
|
|||||||
sampleRowHTML += '</tr>';
|
sampleRowHTML += '</tr>';
|
||||||
|
|
||||||
// Add explanation row if address column is selected
|
// Add explanation row if address column is selected
|
||||||
const hasAddressColumn = selectedColumns.some(cb => cb.value === 'address');
|
const hasAddressColumn = currentOrder.includes('address');
|
||||||
if (hasAddressColumn) {
|
if (hasAddressColumn) {
|
||||||
sampleRowHTML += `
|
sampleRowHTML += `
|
||||||
<tr style="background-color: #f8f9fa; font-size: 0.85em; color: #6c757d;">
|
<tr style="background-color: #f8f9fa; font-size: 0.85em; color: #6c757d;">
|
||||||
<td colspan="${selectedColumns.length}" style="text-align: center; padding: 0.75rem; font-style: italic;">
|
<td colspan="${currentOrder.length}" style="text-align: center; padding: 0.75rem; font-style: italic;">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
* Check-in Address: Shows QR address when location accuracy ≤ 0.5 miles, otherwise shows actual GPS address
|
* Check-in Address: Shows QR address when location accuracy ≤ 0.5 miles, otherwise shows actual GPS address
|
||||||
</td>
|
</td>
|
||||||
@@ -122,20 +330,20 @@ function updatePreview() {
|
|||||||
previewTable.innerHTML = sampleRowHTML;
|
previewTable.innerHTML = sampleRowHTML;
|
||||||
|
|
||||||
// Update generate button
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Error updating preview:', error);
|
console.error('Error updating preview:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSampleData(columnKey) {
|
function getSampleData(columnKey) {
|
||||||
// Return sample data for each column type - Updated with correct event types
|
// Return sample data for each column type
|
||||||
const sampleData = {
|
const sampleData = {
|
||||||
'employee_id': 'EMP001',
|
'employee_id': 'EMP001',
|
||||||
'location_name': 'Main Office',
|
'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_date': '2025-08-14',
|
||||||
'check_in_time': '09:30:00',
|
'check_in_time': '09:30:00',
|
||||||
'qr_address': '123 Business St, City',
|
'qr_address': '123 Business St, City',
|
||||||
@@ -152,51 +360,277 @@ function getSampleData(columnKey) {
|
|||||||
return sampleData[columnKey] || 'Sample Data';
|
return sampleData[columnKey] || 'Sample Data';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 deselectAllColumns() {
|
||||||
|
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() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear saved order
|
||||||
|
savedColumnOrder = [];
|
||||||
|
|
||||||
|
updateSelectedColumnsList();
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
// Clear saved preferences
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('exportPreferences');
|
||||||
|
} catch (storageError) {
|
||||||
|
console.warn('Could not clear saved preferences:', storageError);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Reset to default settings');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resetting to defaults:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateGenerateButton(columnCount) {
|
function updateGenerateButton(columnCount) {
|
||||||
|
try {
|
||||||
const generateBtn = document.getElementById('generateBtn');
|
const generateBtn = document.getElementById('generateBtn');
|
||||||
if (!generateBtn) return;
|
if (!generateBtn) return;
|
||||||
|
|
||||||
if (columnCount === 0) {
|
if (columnCount === 0) {
|
||||||
generateBtn.disabled = true;
|
generateBtn.disabled = true;
|
||||||
generateBtn.innerHTML = 'Select columns to export';
|
generateBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Select columns to export';
|
||||||
|
generateBtn.classList.add('btn-disabled');
|
||||||
} else {
|
} else {
|
||||||
generateBtn.disabled = false;
|
generateBtn.disabled = false;
|
||||||
generateBtn.innerHTML = 'Generate Excel Export (' + columnCount + ' columns)';
|
generateBtn.innerHTML = `<i class="fas fa-download"></i> Generate Excel Export (${columnCount} columns)`;
|
||||||
|
generateBtn.classList.remove('btn-disabled');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating generate button:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAllColumns() {
|
function validateForm() {
|
||||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
try {
|
||||||
checkboxes.forEach(function(checkbox) {
|
const selectedColumns = document.querySelectorAll('input[name="selected_columns"]:checked');
|
||||||
checkbox.checked = true;
|
|
||||||
toggleColumnName(checkbox.value);
|
|
||||||
});
|
|
||||||
updatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deselectAllColumns() {
|
if (selectedColumns.length === 0) {
|
||||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
alert('Please select at least one column to export.');
|
||||||
checkboxes.forEach(function(checkbox) {
|
return false;
|
||||||
checkbox.checked = false;
|
}
|
||||||
toggleColumnName(checkbox.value);
|
|
||||||
});
|
|
||||||
updatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetToDefaults() {
|
// Validate that all selected columns have names
|
||||||
// Get checkboxes and reset to defaults
|
let hasEmptyNames = false;
|
||||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
selectedColumns.forEach(checkbox => {
|
||||||
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);
|
const nameInput = document.getElementById('name_' + checkbox.value);
|
||||||
if (nameInput) {
|
if (nameInput && nameInput.value.trim() === '') {
|
||||||
nameInput.value = nameInput.getAttribute('value') || checkbox.value;
|
hasEmptyNames = true;
|
||||||
|
nameInput.style.borderColor = '#e53e3e';
|
||||||
|
nameInput.focus();
|
||||||
|
} else if (nameInput) {
|
||||||
|
nameInput.style.borderColor = '#e2e8f0';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updatePreview();
|
|
||||||
|
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);
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<!-- Export Configuration specific CSS -->
|
<!-- Export Configuration specific CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/export_configuration.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/export_configuration.css') }}">
|
||||||
|
<!-- Add Sortable.js for drag & drop -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
<i class="fas fa-file-excel"></i>
|
<i class="fas fa-file-excel"></i>
|
||||||
Export Configuration
|
Export Configuration
|
||||||
</h1>
|
</h1>
|
||||||
<p>Customize your attendance report export with selected columns and custom names</p>
|
<p>Customize your attendance report export with selected columns, custom names, and column ordering</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
@@ -76,28 +78,50 @@
|
|||||||
<input type="hidden" name="location_filter" value="{{ filters.location_filter }}">
|
<input type="hidden" name="location_filter" value="{{ filters.location_filter }}">
|
||||||
<input type="hidden" name="employee_filter" value="{{ filters.employee_filter }}">
|
<input type="hidden" name="employee_filter" value="{{ filters.employee_filter }}">
|
||||||
|
|
||||||
|
<!-- Hidden field for column order -->
|
||||||
|
<input type="hidden" id="column_order" name="column_order" value="">
|
||||||
|
|
||||||
<div class="config-card">
|
<div class="config-card">
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<h3>
|
<h3>
|
||||||
<i class="fas fa-columns"></i>
|
<i class="fas fa-columns"></i>
|
||||||
Select Columns to Export
|
Select & Order Columns for Export
|
||||||
</h3>
|
</h3>
|
||||||
<div class="config-actions">
|
<div class="config-actions">
|
||||||
<button type="button" onclick="selectAllColumns()" class="btn btn-sm btn-outline">
|
<button type="button" onclick="selectAllColumns()" class="btn btn-sm btn-outline">
|
||||||
|
<i class="fas fa-check-double"></i>
|
||||||
Select All
|
Select All
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="deselectAllColumns()" class="btn btn-sm btn-outline">
|
<button type="button" onclick="deselectAllColumns()" class="btn btn-sm btn-outline">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
Deselect All
|
Deselect All
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="resetToDefaults()" class="btn btn-sm btn-outline">
|
<button type="button" onclick="resetToDefaults()" class="btn btn-sm btn-outline">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
Reset to Defaults
|
Reset to Defaults
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns-grid">
|
<!-- Instructions -->
|
||||||
|
<div class="instructions-card">
|
||||||
|
<div class="instructions-content">
|
||||||
|
<h4><i class="fas fa-info-circle"></i> How to Use</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Select columns:</strong> Check the boxes for columns you want to export</li>
|
||||||
|
<li><strong>Customize names:</strong> Edit the "Export as" field to change column headers</li>
|
||||||
|
<li><strong>Reorder columns:</strong> <span class="highlight">Drag & drop</span> the selected columns to change their order in the Excel file</li>
|
||||||
|
<li><strong>Preview:</strong> See how your export will look in the preview below</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Available Columns Grid -->
|
||||||
|
<div class="columns-section">
|
||||||
|
<h4><i class="fas fa-list"></i> Available Columns</h4>
|
||||||
|
<div class="columns-grid" id="columnsGrid">
|
||||||
{% for column in available_columns %}
|
{% for column in available_columns %}
|
||||||
<div class="column-item">
|
<div class="column-item" data-column-key="{{ column.key }}">
|
||||||
<div class="column-checkbox">
|
<div class="column-checkbox">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
id="col_{{ column.key }}"
|
id="col_{{ column.key }}"
|
||||||
@@ -125,6 +149,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Columns Ordering Section -->
|
||||||
|
<div class="selected-columns-section" id="selectedColumnsSection" style="display: none;">
|
||||||
|
<h4><i class="fas fa-sort"></i> Column Order in Export <span class="drag-hint">(Drag to reorder)</span></h4>
|
||||||
|
<div class="selected-columns-list" id="selectedColumnsList">
|
||||||
|
<!-- Selected columns will appear here for reordering -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Preview Section -->
|
<!-- Preview Section -->
|
||||||
<div class="preview-section">
|
<div class="preview-section">
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
@@ -160,7 +193,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<p class="submit-note">
|
<p class="submit-note">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Your column preferences will be saved for next time
|
Your column preferences and order will be saved for next time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,13 +207,16 @@
|
|||||||
<script>
|
<script>
|
||||||
// Initialize export configuration
|
// Initialize export configuration
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
console.log('Export Configuration page initialized');
|
console.log('Export Configuration page initialized with drag & drop');
|
||||||
|
|
||||||
// Load saved preferences if available
|
// Load saved preferences if available
|
||||||
loadSavedPreferences();
|
loadSavedPreferences();
|
||||||
|
|
||||||
// Update preview on page load
|
// Update preview on page load
|
||||||
updatePreview();
|
updatePreview();
|
||||||
|
|
||||||
|
// Initialize drag & drop
|
||||||
|
initializeDragDrop();
|
||||||
});
|
});
|
||||||
|
|
||||||
function goBackToReport() {
|
function goBackToReport() {
|
||||||
@@ -226,6 +262,13 @@ function loadSavedPreferences() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply saved column order
|
||||||
|
if (prefs.column_order) {
|
||||||
|
// Column order will be applied when selected columns are updated
|
||||||
|
window.savedColumnOrder = prefs.column_order;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedColumnsList();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -236,6 +279,7 @@ function loadSavedPreferences() {
|
|||||||
// Form submission handler to save preferences
|
// Form submission handler to save preferences
|
||||||
document.getElementById('exportForm').addEventListener('submit', function() {
|
document.getElementById('exportForm').addEventListener('submit', function() {
|
||||||
savePreferences();
|
savePreferences();
|
||||||
|
updateColumnOrderField();
|
||||||
});
|
});
|
||||||
|
|
||||||
function savePreferences() {
|
function savePreferences() {
|
||||||
@@ -250,9 +294,13 @@ function savePreferences() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get current column order from the sortable list
|
||||||
|
const columnOrder = getCurrentColumnOrder();
|
||||||
|
|
||||||
const prefs = {
|
const prefs = {
|
||||||
selected_columns: selectedColumns,
|
selected_columns: selectedColumns,
|
||||||
column_names: columnNames
|
column_names: columnNames,
|
||||||
|
column_order: columnOrder
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem('exportPreferences', JSON.stringify(prefs));
|
localStorage.setItem('exportPreferences', JSON.stringify(prefs));
|
||||||
|
|||||||
Reference in New Issue
Block a user