1116 lines
41 KiB
Python
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)
|