""" Lemontropia Suite - Session History Dialog Displays past hunting sessions with detailed stats and export functionality. """ import json import csv from datetime import datetime, timedelta from decimal import Decimal from pathlib import Path from typing import Optional, List, Dict, Any from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, QHeaderView, QSplitter, QWidget, QGroupBox, QMessageBox, QFileDialog, QComboBox, QLineEdit, QTableWidget, QTableWidgetItem, QTabWidget, QAbstractItemView ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QColor from core.database import DatabaseManager from core.project_manager import HuntingSessionData class SessionHistoryDialog(QDialog): """ Dialog for viewing and managing hunting session history. Features: - List of past sessions with key metrics - Detailed session statistics view - Export to JSON/CSV - Filter by project and date range """ session_selected = pyqtSignal(int) # Emits session_id when selected def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Session History") self.setMinimumSize(1200, 800) self.resize(1400, 900) # Initialize database self.db = DatabaseManager() # State self.current_session_id: Optional[int] = None self.sessions_data: List[Dict[str, Any]] = [] self._setup_ui() self._load_sessions() self._apply_dark_theme() def _setup_ui(self): """Setup the dialog UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(10) # Top controls controls_layout = QHBoxLayout() # Project filter controls_layout.addWidget(QLabel("Project:")) self.project_filter = QComboBox() self.project_filter.addItem("All Projects", None) self.project_filter.currentIndexChanged.connect(self._on_filter_changed) controls_layout.addWidget(self.project_filter) # Date range filter controls_layout.addWidget(QLabel("From:")) self.date_from = QLineEdit() self.date_from.setPlaceholderText("YYYY-MM-DD") self.date_from.setMaximumWidth(120) controls_layout.addWidget(self.date_from) controls_layout.addWidget(QLabel("To:")) self.date_to = QLineEdit() self.date_to.setPlaceholderText("YYYY-MM-DD") self.date_to.setMaximumWidth(120) controls_layout.addWidget(self.date_to) # Search controls_layout.addWidget(QLabel("Search:")) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Weapon, mob, etc...") self.search_input.textChanged.connect(self._on_search_changed) controls_layout.addWidget(self.search_input) controls_layout.addStretch() # Refresh button self.refresh_btn = QPushButton("🔄 Refresh") self.refresh_btn.clicked.connect(self._load_sessions) controls_layout.addWidget(self.refresh_btn) layout.addLayout(controls_layout) # Main splitter splitter = QSplitter(Qt.Orientation.Horizontal) layout.addWidget(splitter) # Left side - Session list left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) # Sessions table self.sessions_table = QTreeWidget() self.sessions_table.setHeaderLabels([ "ID", "Date", "Project", "Duration", "Kills", "Cost", "Loot", "Profit/Loss", "Return %", "Globals", "HoFs" ]) self.sessions_table.setAlternatingRowColors(True) self.sessions_table.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) self.sessions_table.setRootIsDecorated(False) self.sessions_table.itemSelectionChanged.connect(self._on_session_selected) self.sessions_table.itemDoubleClicked.connect(self._on_session_double_clicked) # Configure columns header = self.sessions_table.header() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # ID header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) # Date header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Project header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Duration header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # Kills header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) # Cost header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed) # Loot header.setSectionResizeMode(7, QHeaderView.ResizeMode.Fixed) # Profit header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed) # Return % header.setSectionResizeMode(9, QHeaderView.ResizeMode.Fixed) # Globals header.setSectionResizeMode(10, QHeaderView.ResizeMode.Fixed) # HoFs header.resizeSection(0, 50) header.resizeSection(1, 140) header.resizeSection(3, 80) header.resizeSection(4, 50) header.resizeSection(5, 80) header.resizeSection(6, 80) header.resizeSection(7, 90) header.resizeSection(8, 70) header.resizeSection(9, 60) header.resizeSection(10, 50) left_layout.addWidget(self.sessions_table) # Export buttons export_layout = QHBoxLayout() self.export_json_btn = QPushButton("📄 Export JSON") self.export_json_btn.clicked.connect(self._export_json) self.export_json_btn.setEnabled(False) export_layout.addWidget(self.export_json_btn) self.export_csv_btn = QPushButton("📊 Export CSV") self.export_csv_btn.clicked.connect(self._export_csv) self.export_csv_btn.setEnabled(False) export_layout.addWidget(self.export_csv_btn) self.export_all_btn = QPushButton("📁 Export All") self.export_all_btn.clicked.connect(self._export_all) export_layout.addWidget(self.export_all_btn) export_layout.addStretch() self.delete_btn = QPushButton("🗑️ Delete") self.delete_btn.clicked.connect(self._delete_session) self.delete_btn.setEnabled(False) export_layout.addWidget(self.delete_btn) left_layout.addLayout(export_layout) splitter.addWidget(left_panel) # Right side - Session details self.details_tabs = QTabWidget() self.details_tabs.setEnabled(False) # Summary tab self.summary_tab = self._create_summary_tab() self.details_tabs.addTab(self.summary_tab, "📋 Summary") # Combat tab self.combat_tab = self._create_combat_tab() self.details_tabs.addTab(self.combat_tab, "⚔️ Combat") # Loot tab self.loot_tab = self._create_loot_tab() self.details_tabs.addTab(self.loot_tab, "💰 Loot") # Equipment tab self.equipment_tab = self._create_equipment_tab() self.details_tabs.addTab(self.equipment_tab, "🛡️ Equipment") splitter.addWidget(self.details_tabs) # Set splitter sizes splitter.setSizes([700, 500]) # Close button close_layout = QHBoxLayout() close_layout.addStretch() self.close_btn = QPushButton("Close") self.close_btn.clicked.connect(self.accept) close_layout.addWidget(self.close_btn) layout.addLayout(close_layout) def _create_summary_tab(self) -> QWidget: """Create the summary details tab.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(15, 15, 15, 15) # Session info group info_group = QGroupBox("Session Information") info_layout = QFormLayout(info_group) self.detail_session_id = QLabel("-") info_layout.addRow("Session ID:", self.detail_session_id) self.detail_project = QLabel("-") info_layout.addRow("Project:", self.detail_project) self.detail_started = QLabel("-") info_layout.addRow("Started:", self.detail_started) self.detail_ended = QLabel("-") info_layout.addRow("Ended:", self.detail_ended) self.detail_duration = QLabel("-") info_layout.addRow("Duration:", self.detail_duration) layout.addWidget(info_group) # Financial summary group financial_group = QGroupBox("Financial Summary") financial_layout = QFormLayout(financial_group) self.detail_total_cost = QLabel("-") financial_layout.addRow("Total Cost:", self.detail_total_cost) self.detail_loot_value = QLabel("-") financial_layout.addRow("Loot Value:", self.detail_loot_value) self.detail_shrapnel = QLabel("-") financial_layout.addRow("Shrapnel:", self.detail_shrapnel) self.detail_profit_loss = QLabel("-") financial_layout.addRow("Profit/Loss:", self.detail_profit_loss) self.detail_return_pct = QLabel("-") financial_layout.addRow("Return %:", self.detail_return_pct) self.detail_cost_per_hour = QLabel("-") financial_layout.addRow("Cost/Hour:", self.detail_cost_per_hour) self.detail_profit_per_hour = QLabel("-") financial_layout.addRow("Profit/Hour:", self.detail_profit_per_hour) layout.addWidget(financial_group) # Efficiency metrics efficiency_group = QGroupBox("Efficiency Metrics") efficiency_layout = QFormLayout(efficiency_group) self.detail_dpp = QLabel("-") efficiency_layout.addRow("DPP:", self.detail_dpp) self.detail_cost_per_kill = QLabel("-") efficiency_layout.addRow("Cost/Kill:", self.detail_cost_per_kill) self.detail_loot_per_kill = QLabel("-") efficiency_layout.addRow("Loot/Kill:", self.detail_loot_per_kill) layout.addWidget(efficiency_group) layout.addStretch() return widget def _create_combat_tab(self) -> QWidget: """Create the combat statistics tab.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(15, 15, 15, 15) # Combat stats group combat_group = QGroupBox("Combat Statistics") combat_layout = QFormLayout(combat_group) self.detail_shots_fired = QLabel("-") combat_layout.addRow("Shots Fired:", self.detail_shots_fired) self.detail_shots_missed = QLabel("-") combat_layout.addRow("Shots Missed:", self.detail_shots_missed) self.detail_accuracy = QLabel("-") combat_layout.addRow("Accuracy:", self.detail_accuracy) self.detail_kills = QLabel("-") combat_layout.addRow("Kills:", self.detail_kills) self.detail_kills_per_hour = QLabel("-") combat_layout.addRow("Kills/Hour:", self.detail_kills_per_hour) layout.addWidget(combat_group) # Damage group damage_group = QGroupBox("Damage Statistics") damage_layout = QFormLayout(damage_group) self.detail_damage_dealt = QLabel("-") damage_layout.addRow("Damage Dealt:", self.detail_damage_dealt) self.detail_damage_taken = QLabel("-") damage_layout.addRow("Damage Taken:", self.detail_damage_taken) self.detail_damage_per_kill = QLabel("-") damage_layout.addRow("Damage/Kill:", self.detail_damage_per_kill) self.detail_healing_done = QLabel("-") damage_layout.addRow("Healing Done:", self.detail_healing_done) layout.addWidget(damage_group) # Special events group events_group = QGroupBox("Special Events") events_layout = QFormLayout(events_group) self.detail_globals = QLabel("-") events_layout.addRow("Globals:", self.detail_globals) self.detail_hofs = QLabel("-") events_layout.addRow("HoFs:", self.detail_hofs) self.detail_evades = QLabel("-") events_layout.addRow("Evades:", self.detail_evades) layout.addWidget(events_group) layout.addStretch() return widget def _create_loot_tab(self) -> QWidget: """Create the loot breakdown tab.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(15, 15, 15, 15) # Loot breakdown group loot_group = QGroupBox("Loot Breakdown") loot_layout = QFormLayout(loot_group) self.detail_total_loot = QLabel("-") loot_layout.addRow("Total Loot:", self.detail_total_loot) self.detail_other_loot = QLabel("-") loot_layout.addRow("Marketable Loot:", self.detail_other_loot) self.detail_shrapnel_loot = QLabel("-") loot_layout.addRow("Shrapnel:", self.detail_shrapnel_loot) self.detail_universal_ammo = QLabel("-") loot_layout.addRow("Universal Ammo:", self.detail_universal_ammo) layout.addWidget(loot_group) # Cost breakdown group cost_group = QGroupBox("Cost Breakdown") cost_layout = QFormLayout(cost_group) self.detail_weapon_cost = QLabel("-") cost_layout.addRow("Weapon Cost:", self.detail_weapon_cost) self.detail_armor_cost = QLabel("-") cost_layout.addRow("Armor Cost:", self.detail_armor_cost) self.detail_healing_cost = QLabel("-") cost_layout.addRow("Healing Cost:", self.detail_healing_cost) self.detail_plates_cost = QLabel("-") cost_layout.addRow("Plates Cost:", self.detail_plates_cost) self.detail_enhancer_cost = QLabel("-") cost_layout.addRow("Enhancer Cost:", self.detail_enhancer_cost) layout.addWidget(cost_group) layout.addStretch() return widget def _create_equipment_tab(self) -> QWidget: """Create the equipment used tab.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(15, 15, 15, 15) # Equipment group equip_group = QGroupBox("Equipment Used") equip_layout = QFormLayout(equip_group) self.detail_weapon_name = QLabel("-") equip_layout.addRow("Weapon:", self.detail_weapon_name) self.detail_weapon_dpp = QLabel("-") equip_layout.addRow("Weapon DPP:", self.detail_weapon_dpp) self.detail_armor_name = QLabel("-") equip_layout.addRow("Armor:", self.detail_armor_name) self.detail_fap_name = QLabel("-") equip_layout.addRow("Medical Tool:", self.detail_fap_name) layout.addWidget(equip_group) layout.addStretch() return widget def _load_sessions(self): """Load sessions from database.""" self.sessions_table.clear() self.sessions_data = [] try: # Load projects for filter self._load_projects() # Build query with filters query = """ SELECT hs.id, hs.session_id, hs.started_at, hs.ended_at, hs.total_loot_ped, hs.total_shrapnel_ped, hs.total_other_loot_ped, hs.total_cost_ped, hs.weapon_cost_ped, hs.armor_cost_ped, hs.healing_cost_ped, hs.plates_cost_ped, hs.enhancer_cost_ped, hs.damage_dealt, hs.damage_taken, hs.healing_done, hs.shots_fired, hs.shots_missed, hs.evades, hs.kills, hs.globals_count, hs.hofs_count, hs.weapon_name, hs.weapon_dpp, hs.armor_name, hs.fap_name, p.name as project_name, p.id as project_id FROM hunting_sessions hs JOIN sessions s ON hs.session_id = s.id JOIN projects p ON s.project_id = p.id WHERE 1=1 """ params = [] # Apply project filter project_id = self.project_filter.currentData() if project_id: query += " AND p.id = ?" params.append(project_id) # Apply date filters date_from = self.date_from.text().strip() if date_from: query += " AND hs.started_at >= ?" params.append(date_from) date_to = self.date_to.text().strip() if date_to: query += " AND hs.started_at <= ?" params.append(date_to + " 23:59:59") query += " ORDER BY hs.started_at DESC" cursor = self.db.execute(query, tuple(params)) rows = cursor.fetchall() for row in rows: session_data = dict(row) self.sessions_data.append(session_data) # Calculate derived values duration = "-" if session_data['started_at'] and session_data['ended_at']: start = datetime.fromisoformat(session_data['started_at']) end = datetime.fromisoformat(session_data['ended_at']) duration_secs = (end - start).total_seconds() duration = self._format_duration(duration_secs) total_cost = Decimal(str(session_data['total_cost_ped'] or 0)) other_loot = Decimal(str(session_data['total_other_loot_ped'] or 0)) profit_loss = other_loot - total_cost return_pct = Decimal('0') if total_cost > 0: return_pct = (other_loot / total_cost) * Decimal('100') # Format date started = datetime.fromisoformat(session_data['started_at']) date_str = started.strftime("%Y-%m-%d %H:%M") # Create tree item item = QTreeWidgetItem([ str(session_data['id']), date_str, session_data['project_name'] or "Unknown", duration, str(session_data['kills'] or 0), f"{total_cost:.2f}", f"{other_loot:.2f}", f"{profit_loss:+.2f}", f"{return_pct:.1f}%", str(session_data['globals_count'] or 0), str(session_data['hofs_count'] or 0) ]) # Color coding for profit/loss if profit_loss > 0: item.setForeground(7, QColor("#4caf50")) # Green elif profit_loss < 0: item.setForeground(7, QColor("#f44336")) # Red # Color coding for return % if return_pct >= 100: item.setForeground(8, QColor("#4caf50")) elif return_pct >= 90: item.setForeground(8, QColor("#ff9800")) else: item.setForeground(8, QColor("#f44336")) item.setData(0, Qt.ItemDataRole.UserRole, session_data['id']) self.sessions_table.addTopLevelItem(item) except Exception as e: QMessageBox.warning(self, "Error", f"Failed to load sessions: {e}") def _load_projects(self): """Load projects for filter dropdown.""" current = self.project_filter.currentData() self.project_filter.clear() self.project_filter.addItem("All Projects", None) try: cursor = self.db.execute( "SELECT id, name FROM projects ORDER BY name" ) for row in cursor.fetchall(): self.project_filter.addItem(row['name'], row['id']) # Restore selection if current: idx = self.project_filter.findData(current) if idx >= 0: self.project_filter.setCurrentIndex(idx) except Exception: pass def _on_filter_changed(self): """Handle filter changes.""" self._load_sessions() def _on_search_changed(self): """Handle search text changes.""" search = self.search_input.text().lower() for i in range(self.sessions_table.topLevelItemCount()): item = self.sessions_table.topLevelItem(i) # Get session data for this row session_id = item.data(0, Qt.ItemDataRole.UserRole) session_data = next((s for s in self.sessions_data if s['id'] == session_id), None) if session_data: # Search in weapon name, project name weapon = (session_data.get('weapon_name') or '').lower() project = (session_data.get('project_name') or '').lower() match = search in weapon or search in project item.setHidden(bool(search) and not match) def _on_session_selected(self): """Handle session selection.""" selected = self.sessions_table.selectedItems() if selected: session_id = selected[0].data(0, Qt.ItemDataRole.UserRole) self.current_session_id = session_id self._load_session_details(session_id) self.details_tabs.setEnabled(True) self.export_json_btn.setEnabled(True) self.export_csv_btn.setEnabled(True) self.delete_btn.setEnabled(True) else: self.current_session_id = None self.details_tabs.setEnabled(False) self.export_json_btn.setEnabled(False) self.export_csv_btn.setEnabled(False) self.delete_btn.setEnabled(False) def _on_session_double_clicked(self, item: QTreeWidgetItem, column: int): """Handle double-click on session.""" session_id = item.data(0, Qt.ItemDataRole.UserRole) self.session_selected.emit(session_id) def _load_session_details(self, session_id: int): """Load and display detailed session information.""" try: # Get session data cursor = self.db.execute(""" SELECT hs.*, p.name as project_name, s.started_at, s.ended_at FROM hunting_sessions hs JOIN sessions s ON hs.session_id = s.id JOIN projects p ON s.project_id = p.id WHERE hs.id = ? """, (session_id,)) row = cursor.fetchone() if not row: return data = dict(row) # Calculate derived values total_cost = Decimal(str(data['total_cost_ped'] or 0)) total_loot = Decimal(str(data['total_loot_ped'] or 0)) other_loot = Decimal(str(data['total_other_loot_ped'] or 0)) shrapnel = Decimal(str(data['total_shrapnel_ped'] or 0)) profit_loss = other_loot - total_cost return_pct = Decimal('0') if total_cost > 0: return_pct = (other_loot / total_cost) * Decimal('100') # Duration duration_str = "-" if data['started_at'] and data['ended_at']: start = datetime.fromisoformat(data['started_at']) end = datetime.fromisoformat(data['ended_at']) duration_secs = (end - start).total_seconds() duration_str = self._format_duration(duration_secs) # Update summary tab self.detail_session_id.setText(str(data['id'])) self.detail_project.setText(data['project_name'] or "Unknown") self.detail_started.setText(data['started_at'] or "-") self.detail_ended.setText(data['ended_at'] or "-") self.detail_duration.setText(duration_str) self.detail_total_cost.setText(f"{total_cost:.4f} PED") self.detail_loot_value.setText(f"{other_loot:.4f} PED") self.detail_shrapnel.setText(f"{shrapnel:.4f} PED") # Color code profit/loss profit_text = f"{profit_loss:+.4f} PED" if profit_loss > 0: self.detail_profit_loss.setStyleSheet("color: #4caf50; font-weight: bold;") elif profit_loss < 0: self.detail_profit_loss.setStyleSheet("color: #f44336; font-weight: bold;") else: self.detail_profit_loss.setStyleSheet("") self.detail_profit_loss.setText(profit_text) self.detail_return_pct.setText(f"{return_pct:.2f}%") # Cost/profit per hour if data['started_at'] and data['ended_at']: start = datetime.fromisoformat(data['started_at']) end = datetime.fromisoformat(data['ended_at']) hours = (end - start).total_seconds() / 3600 if hours > 0: cost_per_hour = total_cost / Decimal(str(hours)) profit_per_hour = profit_loss / Decimal(str(hours)) self.detail_cost_per_hour.setText(f"{cost_per_hour:.2f} PED/hr") self.detail_profit_per_hour.setText(f"{profit_per_hour:+.2f} PED/hr") else: self.detail_cost_per_hour.setText("-") self.detail_profit_per_hour.setText("-") else: self.detail_cost_per_hour.setText("-") self.detail_profit_per_hour.setText("-") # DPP and efficiency damage_dealt = Decimal(str(data['damage_dealt'] or 0)) kills = data['kills'] or 0 if total_cost > 0: dpp = damage_dealt / total_cost self.detail_dpp.setText(f"{dpp:.4f}") else: self.detail_dpp.setText("-") if kills > 0: cost_per_kill = total_cost / kills loot_per_kill = other_loot / kills self.detail_cost_per_kill.setText(f"{cost_per_kill:.4f} PED") self.detail_loot_per_kill.setText(f"{loot_per_kill:.4f} PED") else: self.detail_cost_per_kill.setText("-") self.detail_loot_per_kill.setText("-") # Update combat tab self.detail_shots_fired.setText(str(data['shots_fired'] or 0)) self.detail_shots_missed.setText(str(data['shots_missed'] or 0)) shots_fired = data['shots_fired'] or 0 shots_missed = data['shots_missed'] or 0 total_shots = shots_fired + shots_missed if total_shots > 0: accuracy = (shots_fired / total_shots) * 100 self.detail_accuracy.setText(f"{accuracy:.1f}%") else: self.detail_accuracy.setText("-") self.detail_kills.setText(str(kills)) # Kills per hour if data['started_at'] and data['ended_at']: hours = (end - start).total_seconds() / 3600 if hours > 0: kph = kills / hours self.detail_kills_per_hour.setText(f"{kph:.1f}") else: self.detail_kills_per_hour.setText("-") else: self.detail_kills_per_hour.setText("-") self.detail_damage_dealt.setText(f"{damage_dealt:.0f}") self.detail_damage_taken.setText(f"{data['damage_taken'] or 0:.0f}") if kills > 0: dpk = damage_dealt / kills self.detail_damage_per_kill.setText(f"{dpk:.0f}") else: self.detail_damage_per_kill.setText("-") self.detail_healing_done.setText(f"{data['healing_done'] or 0:.0f}") self.detail_globals.setText(str(data['globals_count'] or 0)) self.detail_hofs.setText(str(data['hofs_count'] or 0)) self.detail_evades.setText(str(data['evades'] or 0)) # Update loot tab self.detail_total_loot.setText(f"{total_loot:.4f} PED") self.detail_other_loot.setText(f"{other_loot:.4f} PED") self.detail_shrapnel_loot.setText(f"{shrapnel:.4f} PED") self.detail_universal_ammo.setText(f"{data['total_universal_ammo_ped'] or 0:.4f} PED") self.detail_weapon_cost.setText(f"{data['weapon_cost_ped'] or 0:.4f} PED") self.detail_armor_cost.setText(f"{data['armor_cost_ped'] or 0:.4f} PED") self.detail_healing_cost.setText(f"{data['healing_cost_ped'] or 0:.4f} PED") self.detail_plates_cost.setText(f"{data['plates_cost_ped'] or 0:.4f} PED") self.detail_enhancer_cost.setText(f"{data['enhancer_cost_ped'] or 0:.4f} PED") # Update equipment tab self.detail_weapon_name.setText(data['weapon_name'] or "-") self.detail_weapon_dpp.setText(f"{data['weapon_dpp'] or 0:.4f}") self.detail_armor_name.setText(data['armor_name'] or "-") self.detail_fap_name.setText(data['fap_name'] or "-") except Exception as e: QMessageBox.warning(self, "Error", f"Failed to load session details: {e}") def _format_duration(self, seconds: float) -> str: """Format duration in seconds to human readable string.""" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) if hours > 0: return f"{hours}h {minutes}m" else: return f"{minutes}m" def _export_json(self): """Export selected session to JSON.""" if not self.current_session_id: return file_path, _ = QFileDialog.getSaveFileName( self, "Export Session", f"session_{self.current_session_id}.json", "JSON Files (*.json)" ) if not file_path: return try: # Get full session data session_data = self._get_full_session_data(self.current_session_id) with open(file_path, 'w') as f: json.dump(session_data, f, indent=2, default=str) QMessageBox.information(self, "Success", f"Session exported to {file_path}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to export: {e}") def _export_csv(self): """Export selected session to CSV.""" if not self.current_session_id: return file_path, _ = QFileDialog.getSaveFileName( self, "Export Session", f"session_{self.current_session_id}.csv", "CSV Files (*.csv)" ) if not file_path: return try: session_data = self._get_full_session_data(self.current_session_id) with open(file_path, 'w', newline='') as f: writer = csv.writer(f) # Write header and values for key, value in session_data.items(): writer.writerow([key, value]) QMessageBox.information(self, "Success", f"Session exported to {file_path}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to export: {e}") def _export_all(self): """Export all sessions to CSV.""" file_path, _ = QFileDialog.getSaveFileName( self, "Export All Sessions", "sessions_export.csv", "CSV Files (*.csv)" ) if not file_path: return try: with open(file_path, 'w', newline='') as f: writer = csv.writer(f) # Header writer.writerow([ 'ID', 'Date', 'Project', 'Duration', 'Kills', 'Cost', 'Loot', 'Profit/Loss', 'Return %', 'Globals', 'HoFs', 'Weapon', 'DPP' ]) # Data rows for session in self.sessions_data: started = datetime.fromisoformat(session['started_at']) duration = "" if session['ended_at']: end = datetime.fromisoformat(session['ended_at']) duration = self._format_duration((end - started).total_seconds()) total_cost = Decimal(str(session['total_cost_ped'] or 0)) other_loot = Decimal(str(session['total_other_loot_ped'] or 0)) profit_loss = other_loot - total_cost return_pct = Decimal('0') if total_cost > 0: return_pct = (other_loot / total_cost) * Decimal('100') writer.writerow([ session['id'], started.strftime("%Y-%m-%d %H:%M"), session['project_name'], duration, session['kills'] or 0, float(total_cost), float(other_loot), float(profit_loss), float(return_pct), session['globals_count'] or 0, session['hofs_count'] or 0, session['weapon_name'] or '', session['weapon_dpp'] or 0 ]) QMessageBox.information(self, "Success", f"All sessions exported to {file_path}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to export: {e}") def _get_full_session_data(self, session_id: int) -> Dict[str, Any]: """Get complete session data including related records.""" cursor = self.db.execute(""" SELECT hs.*, p.name as project_name, s.started_at, s.ended_at, s.notes as session_notes FROM hunting_sessions hs JOIN sessions s ON hs.session_id = s.id JOIN projects p ON s.project_id = p.id WHERE hs.id = ? """, (session_id,)) session = dict(cursor.fetchone()) # Get loot events cursor = self.db.execute(""" SELECT * FROM loot_events WHERE session_id = ? ORDER BY timestamp """, (session['session_id'],)) session['loot_events'] = [dict(row) for row in cursor.fetchall()] # Get combat events cursor = self.db.execute(""" SELECT * FROM combat_events WHERE session_id = ? ORDER BY timestamp """, (session['session_id'],)) session['combat_events'] = [dict(row) for row in cursor.fetchall()] # Get skill gains cursor = self.db.execute(""" SELECT * FROM skill_gains WHERE session_id = ? ORDER BY timestamp """, (session['session_id'],)) session['skill_gains'] = [dict(row) for row in cursor.fetchall()] return session def _delete_session(self): """Delete the selected session.""" if not self.current_session_id: return reply = QMessageBox.question( self, "Confirm Delete", "Are you sure you want to delete this session?\n\n" "This will permanently remove all session data including loot, combat, and skill records.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return try: # Get session_id from hunting_sessions cursor = self.db.execute( "SELECT session_id FROM hunting_sessions WHERE id = ?", (self.current_session_id,) ) row = cursor.fetchone() if row: session_id = row['session_id'] # Delete related records first self.db.execute("DELETE FROM loot_events WHERE session_id = ?", (session_id,)) self.db.execute("DELETE FROM combat_events WHERE session_id = ?", (session_id,)) self.db.execute("DELETE FROM skill_gains WHERE session_id = ?", (session_id,)) self.db.execute("DELETE FROM decay_events WHERE session_id = ?", (session_id,)) self.db.execute("DELETE FROM screenshots WHERE session_id = ?", (session_id,)) # Delete hunting session self.db.execute("DELETE FROM hunting_sessions WHERE id = ?", (self.current_session_id,)) # Delete main session self.db.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) self.db.commit() self._load_sessions() self.details_tabs.setEnabled(False) self.current_session_id = None QMessageBox.information(self, "Success", "Session deleted successfully") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to delete session: {e}") def _apply_dark_theme(self): """Apply dark theme styling.""" dark_stylesheet = """ QDialog { background-color: #1e1e1e; } QWidget { background-color: #1e1e1e; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 10pt; } QGroupBox { font-weight: bold; border: 1px solid #444; border-radius: 6px; margin-top: 10px; padding-top: 10px; padding: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #888; } QPushButton { background-color: #2d2d2d; border: 1px solid #444; border-radius: 4px; padding: 8px 16px; color: #e0e0e0; } QPushButton:hover { background-color: #3d3d3d; border-color: #555; } QPushButton:pressed { background-color: #4d4d4d; } QPushButton:disabled { background-color: #252525; color: #666; border-color: #333; } QTreeWidget { background-color: #252525; border: 1px solid #444; border-radius: 4px; outline: none; } QTreeWidget::item { padding: 6px; border-bottom: 1px solid #333; } QTreeWidget::item:selected { background-color: #0d47a1; color: white; } QTreeWidget::item:alternate { background-color: #2a2a2a; } QHeaderView::section { background-color: #2d2d2d; padding: 6px; border: none; border-right: 1px solid #444; font-weight: bold; } QTabWidget::pane { border: 1px solid #444; background-color: #1e1e1e; } QTabBar::tab { background-color: #2d2d2d; border: 1px solid #444; padding: 8px 16px; margin-right: 2px; } QTabBar::tab:selected { background-color: #0d47a1; } QTabBar::tab:hover:!selected { background-color: #3d3d3d; } QLineEdit { background-color: #252525; border: 1px solid #444; border-radius: 4px; padding: 6px; color: #e0e0e0; } QLineEdit:focus { border-color: #0d47a1; } QComboBox { background-color: #252525; border: 1px solid #444; border-radius: 4px; padding: 6px; color: #e0e0e0; } QComboBox:focus { border-color: #0d47a1; } QComboBox::drop-down { border: none; padding-right: 10px; } QLabel { color: #e0e0e0; } QFormLayout QLabel { color: #888; } QSplitter::handle { background-color: #444; } """ self.setStyleSheet(dark_stylesheet)