import tkinter as tk from tkinter import ttk, filedialog, messagebox import pandas as pd from pathlib import Path from data_comparator import KSTCoordiComparator class DataComparisonGUI: def __init__(self, root): self.root = root self.root.title("KST vs Coordi Data Comparison Tool") self.root.geometry("1200x800") self.comparator = None self.comparison_data = None self.setup_ui() def setup_ui(self): # Main container main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # Configure grid weights self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(2, weight=1) # Title title_label = ttk.Label(main_frame, text="KST vs Coordi Data Comparison", font=("Arial", 16, "bold")) title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) # File selection frame file_frame = ttk.LabelFrame(main_frame, text="File Selection", padding="10") file_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) file_frame.columnconfigure(1, weight=1) ttk.Label(file_frame, text="Excel File:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) self.file_path_var = tk.StringVar(value="data/sample-data.xlsx") self.file_entry = ttk.Entry(file_frame, textvariable=self.file_path_var, width=50) self.file_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10)) browse_btn = ttk.Button(file_frame, text="Browse", command=self.browse_file) browse_btn.grid(row=0, column=2) analyze_btn = ttk.Button(file_frame, text="Analyze Data", command=self.analyze_data) analyze_btn.grid(row=0, column=3, padx=(10, 0)) # Results notebook (tabs) self.notebook = ttk.Notebook(main_frame) self.notebook.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S)) # Create tabs self.create_summary_tab() self.create_matched_tab() self.create_kst_only_tab() self.create_coordi_only_tab() # Status bar self.status_var = tk.StringVar(value="Ready - Select an Excel file and click 'Analyze Data'") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN) status_bar.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(10, 0)) def create_summary_tab(self): # Summary tab summary_frame = ttk.Frame(self.notebook) self.notebook.add(summary_frame, text="Summary") # Configure grid summary_frame.columnconfigure(0, weight=1) summary_frame.rowconfigure(1, weight=1) # Summary text widget summary_text_frame = ttk.LabelFrame(summary_frame, text="Comparison Summary", padding="10") summary_text_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10, pady=10) summary_text_frame.columnconfigure(0, weight=1) summary_text_frame.rowconfigure(0, weight=1) self.summary_text = tk.Text(summary_text_frame, wrap=tk.WORD, height=15) summary_scrollbar = ttk.Scrollbar(summary_text_frame, orient=tk.VERTICAL, command=self.summary_text.yview) self.summary_text.configure(yscrollcommand=summary_scrollbar.set) self.summary_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) summary_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) # Reconciliation info reconcile_frame = ttk.LabelFrame(summary_frame, text="Reconciliation Results", padding="10") reconcile_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), padx=10, pady=(0, 10)) self.reconcile_text = tk.Text(reconcile_frame, wrap=tk.WORD, height=8) reconcile_scrollbar = ttk.Scrollbar(reconcile_frame, orient=tk.VERTICAL, command=self.reconcile_text.yview) self.reconcile_text.configure(yscrollcommand=reconcile_scrollbar.set) self.reconcile_text.grid(row=0, column=0, sticky=(tk.W, tk.E)) reconcile_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) reconcile_frame.columnconfigure(0, weight=1) def create_matched_tab(self): matched_frame = ttk.Frame(self.notebook) self.notebook.add(matched_frame, text="Matched Items") self.create_data_table(matched_frame, "matched") def create_kst_only_tab(self): kst_frame = ttk.Frame(self.notebook) self.notebook.add(kst_frame, text="KST Only") self.create_data_table(kst_frame, "kst_only") def create_coordi_only_tab(self): coordi_frame = ttk.Frame(self.notebook) self.notebook.add(coordi_frame, text="Coordi Only") self.create_data_table(coordi_frame, "coordi_only") def create_data_table(self, parent, table_type): # Configure grid parent.columnconfigure(0, weight=1) parent.rowconfigure(1, weight=1) # Info label info_frame = ttk.Frame(parent) info_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=10, pady=10) info_frame.columnconfigure(1, weight=1) count_label = ttk.Label(info_frame, text="Count:") count_label.grid(row=0, column=0, padx=(0, 10)) count_var = tk.StringVar(value="0") setattr(self, f"{table_type}_count_var", count_var) count_display = ttk.Label(info_frame, textvariable=count_var, font=("Arial", 10, "bold")) count_display.grid(row=0, column=1, sticky=tk.W) # Table frame table_frame = ttk.Frame(parent) table_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10, pady=(0, 10)) table_frame.columnconfigure(0, weight=1) table_frame.rowconfigure(0, weight=1) # Create treeview columns = ("Title", "Episode", "Sheet", "Row", "Reason") tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=20) # Configure columns tree.heading("Title", text="Title") tree.heading("Episode", text="Episode") tree.heading("Sheet", text="Sheet") tree.heading("Row", text="Row") tree.heading("Reason", text="Reason") tree.column("Title", width=300) tree.column("Episode", width=100) tree.column("Sheet", width=120) tree.column("Row", width=80) tree.column("Reason", width=300) # Scrollbars v_scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=tree.yview) h_scrollbar = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=tree.xview) tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) v_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) h_scrollbar.grid(row=1, column=0, sticky=(tk.W, tk.E)) # Store tree widget setattr(self, f"{table_type}_tree", tree) def browse_file(self): file_path = filedialog.askopenfilename( title="Select Excel File", filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")] ) if file_path: self.file_path_var.set(file_path) def analyze_data(self): file_path = self.file_path_var.get().strip() if not file_path: messagebox.showerror("Error", "Please select an Excel file") return if not Path(file_path).exists(): messagebox.showerror("Error", f"File not found: {file_path}") return try: self.status_var.set("Analyzing data...") self.root.update() # Create comparator and analyze self.comparator = KSTCoordiComparator(file_path) if not self.comparator.load_data(): messagebox.showerror("Error", "Failed to load Excel data") return # Get comparison results self.comparison_data = self.comparator.get_comparison_summary() # Update GUI self.update_summary() self.update_data_tables() self.status_var.set("Analysis complete!") except Exception as e: messagebox.showerror("Error", f"Analysis failed: {str(e)}") self.status_var.set("Analysis failed") def update_summary(self): if not self.comparison_data: return # Clear previous content self.summary_text.delete(1.0, tk.END) self.reconcile_text.delete(1.0, tk.END) data = self.comparison_data # Summary text summary = f"""COMPARISON SUMMARY {'='*50} Original Counts: KST Total: {data['original_counts']['kst_total']:,} Coordi Total: {data['original_counts']['coordi_total']:,} Matched Items: {data['matched_items_count']:,} Mismatches: KST Only: {data['mismatches']['kst_only_count']:,} Coordi Only: {data['mismatches']['coordi_only_count']:,} KST Duplicates: {data['mismatches']['kst_duplicates_count']:,} Coordi Duplicates: {data['mismatches']['coordi_duplicates_count']:,} Total Mismatches: {data['mismatches']['kst_only_count'] + data['mismatches']['coordi_only_count'] + data['mismatches']['kst_duplicates_count'] + data['mismatches']['coordi_duplicates_count']:,} """ self.summary_text.insert(tk.END, summary) # Reconciliation text reconcile = data['reconciliation'] reconcile_info = f"""RECONCILIATION RESULTS {'='*40} After excluding mismatches: KST Count: {reconcile['reconciled_kst_count']:,} Coordi Count: {reconcile['reconciled_coordi_count']:,} Counts Match: {'✅ YES' if reconcile['counts_match_after_reconciliation'] else '❌ NO'} Items to exclude: From KST: {reconcile['items_to_exclude_from_kst']:,} From Coordi: {reconcile['items_to_exclude_from_coordi']:,} Final Result: Both datasets will have {reconcile['reconciled_kst_count']:,} matching items after reconciliation. """ self.reconcile_text.insert(tk.END, reconcile_info) def update_data_tables(self): if not self.comparison_data: return mismatches = self.comparison_data['mismatch_details'] # Update matched items (create from intersection) matched_count = self.comparison_data['matched_items_count'] self.matched_count_var.set(f"{matched_count:,}") # Clear matched tree for item in self.matched_tree.get_children(): self.matched_tree.delete(item) # Add matched items (we'll show the first few as examples) if self.comparator: categorization = self.comparator.categorize_mismatches() matched_items = categorization['matched_items'] for i, item in enumerate(list(matched_items)[:100]): # Show first 100 self.matched_tree.insert("", tk.END, values=( item.title, item.episode, item.source_sheet, item.row_index + 1, "Perfect match" )) # Update KST only kst_only = mismatches['kst_only'] self.kst_only_count_var.set(f"{len(kst_only):,}") for item in self.kst_only_tree.get_children(): self.kst_only_tree.delete(item) for mismatch in kst_only: self.kst_only_tree.insert("", tk.END, values=( mismatch['title'], mismatch['episode'], mismatch['sheet'], mismatch['row_index'] + 1, mismatch['reason'] )) # Update Coordi only coordi_only = mismatches['coordi_only'] self.coordi_only_count_var.set(f"{len(coordi_only):,}") for item in self.coordi_only_tree.get_children(): self.coordi_only_tree.delete(item) for mismatch in coordi_only: self.coordi_only_tree.insert("", tk.END, values=( mismatch['title'], mismatch['episode'], mismatch['sheet'], mismatch['row_index'] + 1, mismatch['reason'] )) def main(): root = tk.Tk() app = DataComparisonGUI(root) root.mainloop() if __name__ == "__main__": main()