- Migrate from SQLite to PostgreSQL with normalized schema - Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission, materials, position_part, manufacture_type, quality_tier, countries, reference_type, shapes) - Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries - Rewrite console/db.py (27 methods) using SQLAlchemy ORM - Add models.py with 27 SQLAlchemy model definitions - Add config.py for centralized DB_URL configuration - Add migrate_to_postgres.py migration script - Add docs/METABASE_GUIDE.md with complete data entry guide - Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS" - Fill vehicle data gaps via NHTSA API + heuristics: engines (cylinders, power, torque), brands (country, founded_year), models (body_type, production years), MYE (drivetrain, transmission, trim) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
168 lines
5.3 KiB
Python
168 lines
5.3 KiB
Python
"""
|
|
Statistics dashboard screen for the NEXUS AUTOPARTS console application.
|
|
|
|
Displays database table counts and coverage metrics retrieved via
|
|
:meth:`Database.get_stats`.
|
|
"""
|
|
|
|
from console.core.screens import Screen
|
|
from console.core.keybindings import Key
|
|
from console.config import APP_NAME, VERSION
|
|
from console.utils.formatting import format_number
|
|
|
|
|
|
# Human-readable labels for each database table counter.
|
|
_TABLE_LABELS = [
|
|
("brands", "Marcas"),
|
|
("models", "Modelos"),
|
|
("engines", "Motores"),
|
|
("years", "Anos"),
|
|
("part_categories", "Categorias"),
|
|
("part_groups", "Grupos de Partes"),
|
|
("parts", "Partes OEM"),
|
|
("aftermarket_parts", "Partes Aftermarket"),
|
|
("manufacturers", "Fabricantes"),
|
|
("part_cross_references","Cross-References"),
|
|
]
|
|
|
|
# Footer key labels
|
|
_FOOTER = [
|
|
("F5", "Refrescar"),
|
|
("F10", "Menu"),
|
|
("ESC", "Atras"),
|
|
]
|
|
|
|
|
|
class EstadisticasScreen(Screen):
|
|
"""Read-only statistics dashboard showing database counters."""
|
|
|
|
def __init__(self):
|
|
super().__init__(name="estadisticas", title="Estadisticas del Sistema")
|
|
self._stats = None
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _load_stats(self, db):
|
|
"""Fetch fresh statistics from the database."""
|
|
try:
|
|
self._stats = db.get_stats()
|
|
except Exception:
|
|
self._stats = None
|
|
|
|
def _build_fields(self):
|
|
"""Build the detail fields list from the cached stats dict."""
|
|
if self._stats is None:
|
|
return [("Error", "No se pudieron cargar las estadisticas")]
|
|
|
|
fields = []
|
|
|
|
# -- Section: BASE DE DATOS --
|
|
for key, label in _TABLE_LABELS:
|
|
value = self._stats.get(key, 0)
|
|
fields.append((label, format_number(value)))
|
|
|
|
return fields
|
|
|
|
def _build_coverage_fields(self):
|
|
"""Build coverage / summary fields."""
|
|
if self._stats is None:
|
|
return []
|
|
|
|
fields = []
|
|
|
|
# Vehicle-part fitments
|
|
fitments = self._stats.get("vehicle_parts", 0)
|
|
fields.append(("Fitments", format_number(fitments)))
|
|
|
|
# Top brands by fitment count
|
|
top_brands = self._stats.get("top_brands", [])
|
|
if top_brands:
|
|
parts = []
|
|
for b in top_brands[:5]:
|
|
parts.append(f"{b['name']}({format_number(b['count'])})")
|
|
fields.append(("Top marcas", " ".join(parts)))
|
|
|
|
return fields
|
|
|
|
# ------------------------------------------------------------------
|
|
# Screen interface
|
|
# ------------------------------------------------------------------
|
|
|
|
def render(self, context, db, renderer):
|
|
# Load stats on first render (or after refresh)
|
|
if self._stats is None:
|
|
self._load_stats(db)
|
|
|
|
# Header
|
|
renderer.draw_header(
|
|
f" {APP_NAME} v{VERSION}",
|
|
" Estadisticas ",
|
|
)
|
|
|
|
h, w = renderer.get_size()
|
|
|
|
# -- Section title: BASE DE DATOS --
|
|
section_title = " BASE DE DATOS "
|
|
border_char = "\u2500" # ─
|
|
pad_len = max(w - 4 - len(section_title), 0)
|
|
section_line = border_char * 2 + section_title + border_char * pad_len
|
|
renderer.draw_text(3, 2, section_line[:w - 4], "title")
|
|
|
|
# Database counters
|
|
db_fields = self._build_fields()
|
|
max_label = max((len(lbl) for lbl, _ in db_fields), default=10)
|
|
dot_total = max_label + 4
|
|
|
|
row = 5
|
|
for label, value in db_fields:
|
|
if row >= h - 6:
|
|
break
|
|
dots = "." * (dot_total - len(label))
|
|
label_part = f" {label}{dots}: "
|
|
renderer.draw_text(row, 0, label_part, "field_label")
|
|
renderer.draw_text(row, len(label_part), str(value), "field_value")
|
|
row += 1
|
|
|
|
# -- Section title: COBERTURA --
|
|
row += 1
|
|
if row < h - 5:
|
|
section_title2 = " COBERTURA "
|
|
pad_len2 = max(w - 4 - len(section_title2), 0)
|
|
section_line2 = border_char * 2 + section_title2 + border_char * pad_len2
|
|
renderer.draw_text(row, 2, section_line2[:w - 4], "title")
|
|
row += 2
|
|
|
|
coverage_fields = self._build_coverage_fields()
|
|
cov_max_label = max(
|
|
(len(lbl) for lbl, _ in coverage_fields), default=10
|
|
)
|
|
cov_dot_total = cov_max_label + 4
|
|
|
|
for label, value in coverage_fields:
|
|
if row >= h - 3:
|
|
break
|
|
dots = "." * (cov_dot_total - len(label))
|
|
label_part = f" {label}{dots}: "
|
|
renderer.draw_text(row, 0, label_part, "field_label")
|
|
renderer.draw_text(
|
|
row, len(label_part), str(value), "field_value"
|
|
)
|
|
row += 1
|
|
|
|
# Footer
|
|
renderer.draw_footer(_FOOTER)
|
|
|
|
def on_key(self, key, context, db, renderer, nav):
|
|
# F5: refresh stats
|
|
if key == Key.F5:
|
|
self._stats = None # will reload on next render
|
|
return None
|
|
|
|
# ESC or Backspace: go back
|
|
if key in (Key.ESCAPE, Key.BACKSPACE):
|
|
return "back"
|
|
|
|
return None
|