Merge pull request 'add excel file export' (#3) from dev/viettran into main

Reviewed-on: #3
This commit is contained in:
IDS-Viet 2025-08-22 01:54:27 +00:00
commit 5545c5343a
3 changed files with 238 additions and 3 deletions

View File

@ -646,6 +646,33 @@ class KSTCoordiComparator:
return visualize_rows
def generate_excel_export_data(self) -> Dict[str, List[Dict[str, Any]]]:
"""Generate data for Excel export with all sheets in visualize format"""
export_data = {}
# Get all sheet names
sheet_names = list(self.data.keys()) if self.data else []
for sheet_name in sheet_names:
# Generate visualize data for each sheet
sheet_visualize_data = self.generate_visualize_data(sheet_name)
# Convert to Excel-friendly format
excel_rows = []
for row in sheet_visualize_data:
excel_rows.append({
'Coordi Title': row.get('coordi_title', ''),
'Coordi Chapter': row.get('coordi_chapter', ''),
'KST Title': row.get('kst_title', ''),
'KST Chapter': row.get('kst_chapter', ''),
'Status': row.get('reason', ''),
'Type': row.get('row_type', '').replace('_', ' ').title()
})
export_data[sheet_name] = excel_rows
return export_data
def print_comparison_summary(self, sheet_filter: str | None = None):
"""Print a formatted summary of the comparison for a specific sheet"""

View File

@ -284,7 +284,10 @@
</div>
<div id="visualize" class="tab-content">
<h3>Data </h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3>Excel-like Visualization</h3>
<button onclick="downloadExcel()" id="downloadBtn" style="background: #28a745; padding: 8px 16px;">📥 Download All Sheets</button>
</div>
<div class="table-container">
<table id="visualize-table">
<thead>
@ -670,6 +673,60 @@
});
}
function downloadExcel() {
const downloadBtn = document.getElementById('downloadBtn');
const originalText = downloadBtn.textContent;
// Show loading state
downloadBtn.disabled = true;
downloadBtn.textContent = '⏳ Generating...';
downloadBtn.style.background = '#6c757d';
// Create a temporary link and trigger download
fetch('/download_excel', {
method: 'GET'
})
.then(response => {
if (!response.ok) {
throw new Error('Download failed');
}
return response.blob();
})
.then(blob => {
// Create download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'data_comparison_export.xlsx';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
// Show success message
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = '<div class="success">Excel file downloaded successfully!</div>';
setTimeout(() => {
statusDiv.innerHTML = '';
}, 3000);
})
.catch(error => {
console.error('Download error:', error);
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = '<div class="error">Download failed. Please try again.</div>';
setTimeout(() => {
statusDiv.innerHTML = '';
}, 5000);
})
.finally(() => {
// Reset button state
downloadBtn.disabled = false;
downloadBtn.textContent = originalText;
downloadBtn.style.background = '#28a745';
});
}
// Auto-analyze on page load with default file
window.onload = function() {
// Initialize sheet filter with loading state

View File

@ -1,7 +1,8 @@
from flask import Flask, render_template, request, jsonify
from flask import Flask, render_template, request, jsonify, send_file
import json
import os
import tempfile
import pandas as pd
from pathlib import Path
from werkzeug.utils import secure_filename
from data_comparator import KSTCoordiComparator
@ -131,6 +132,99 @@ def get_sheets():
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/download_excel')
def download_excel():
"""Generate and download Excel file with all sheets in visualize format"""
global comparator_instance
try:
if not comparator_instance:
return jsonify({'error': 'No data available. Please analyze data first.'}), 400
# Generate export data for all sheets
export_data = comparator_instance.generate_excel_export_data()
if not export_data:
return jsonify({'error': 'No data available for export'}), 400
# Create temporary Excel file
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx')
temp_path = temp_file.name
temp_file.close()
try:
# Create Excel writer with multiple sheets
with pd.ExcelWriter(temp_path, engine='openpyxl') as writer:
for sheet_name, sheet_data in export_data.items():
if sheet_data: # Only create sheet if there's data
df = pd.DataFrame(sheet_data)
# Clean sheet name for Excel (remove invalid characters)
clean_sheet_name = str(sheet_name).replace('/', '_').replace('\\', '_')[:31]
df.to_excel(writer, sheet_name=clean_sheet_name, index=False)
# Get the workbook and worksheet to apply formatting
workbook = writer.book
worksheet = writer.sheets[clean_sheet_name]
# Apply color formatting based on Type column
from openpyxl.styles import PatternFill
# Define colors matching the web interface
colors = {
'Coordi Only': PatternFill(start_color='FF4444', end_color='FF4444', fill_type='solid'),
'Kst Only': PatternFill(start_color='4488FF', end_color='4488FF', fill_type='solid'),
'Mixed Duplicate': PatternFill(start_color='FF8800', end_color='FF8800', fill_type='solid'),
'Pure Duplicate': PatternFill(start_color='8844FF', end_color='8844FF', fill_type='solid'),
'Matched': PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid')
}
# Find the Type column (should be column F, index 5)
type_col_idx = None
for idx, col in enumerate(df.columns):
if col == 'Type':
type_col_idx = idx + 1 # Excel is 1-indexed
break
# Apply formatting to data rows (skip header)
if type_col_idx:
for row_idx, row_data in enumerate(sheet_data, start=2): # Start from row 2 (after header)
row_type = row_data.get('Type', '')
fill = colors.get(row_type)
if fill:
for col_idx in range(1, len(df.columns) + 1):
cell = worksheet.cell(row=row_idx, column=col_idx)
cell.fill = fill
# Auto-adjust column widths
for column in worksheet.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) # Cap at 50 characters
worksheet.column_dimensions[column_letter].width = adjusted_width
# Send file for download
return send_file(
temp_path,
as_attachment=True,
download_name='data_comparison_export.xlsx',
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
finally:
# Clean up temporary file after a delay (Flask handles this)
pass
except Exception as e:
return jsonify({'error': f'Export failed: {str(e)}'}), 500
def create_templates_dir():
"""Create templates directory and HTML file"""
templates_dir = Path('templates')
@ -422,7 +516,10 @@ def create_templates_dir():
</div>
<div id="visualize" class="tab-content">
<h3>Data </h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3>Excel-like Visualization</h3>
<button onclick="downloadExcel()" id="downloadBtn" style="background: #28a745; padding: 8px 16px;">📥 Download All Sheets</button>
</div>
<div class="table-container">
<table id="visualize-table">
<thead>
@ -808,6 +905,60 @@ def create_templates_dir():
});
}
function downloadExcel() {
const downloadBtn = document.getElementById('downloadBtn');
const originalText = downloadBtn.textContent;
// Show loading state
downloadBtn.disabled = true;
downloadBtn.textContent = '⏳ Generating...';
downloadBtn.style.background = '#6c757d';
// Create a temporary link and trigger download
fetch('/download_excel', {
method: 'GET'
})
.then(response => {
if (!response.ok) {
throw new Error('Download failed');
}
return response.blob();
})
.then(blob => {
// Create download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'data_comparison_export.xlsx';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
// Show success message
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = '<div class="success">Excel file downloaded successfully!</div>';
setTimeout(() => {
statusDiv.innerHTML = '';
}, 3000);
})
.catch(error => {
console.error('Download error:', error);
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = '<div class="error">Download failed. Please try again.</div>';
setTimeout(() => {
statusDiv.innerHTML = '';
}, 5000);
})
.finally(() => {
// Reset button state
downloadBtn.disabled = false;
downloadBtn.textContent = originalText;
downloadBtn.style.background = '#28a745';
});
}
// Auto-analyze on page load with default file
window.onload = function() {
// Initialize sheet filter with loading state