Lemontropia-Suite/ui/session_history.py

1116 lines
41 KiB
Python

"""
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)