Files
Autoparts-DB/console/screens/comparador.py
2026-02-15 01:52:46 +00:00

283 lines
10 KiB
Python

"""
Part comparator screen for the AUTOPARTES console application.
Displays a side-by-side comparison of an OEM part against its aftermarket
alternatives. The first column is always the OEM part; subsequent columns
are aftermarket options. Below the comparison table, cross-reference
numbers are shown grouped by type.
"""
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_currency, quality_bar
# Footer labels
_FOOTER = [
("\u2190\u2192", "Scroll"),
("#", "Ver detalle"),
("F3", "Otra parte"),
("ESC", "Atras"),
]
class ComparadorScreen(Screen):
"""Side-by-side OEM vs aftermarket comparison."""
def __init__(self):
super().__init__(name="comparador", title="Comparador")
self._part = None
self._alternatives = []
self._cross_refs = []
self._manufacturers = {} # id -> dict
self._col_offset = 0 # horizontal scroll offset
self._selected_alt = 0 # currently highlighted alternative
# ------------------------------------------------------------------
# Data loading
# ------------------------------------------------------------------
def _load(self, context, db):
"""Load OEM part, alternatives, cross-refs, and manufacturer info."""
part_id = context.get("part_id")
if part_id is None:
self._part = None
self._alternatives = []
self._cross_refs = []
return
self._part = db.get_part(part_id)
self._alternatives = db.get_alternatives(part_id) if self._part else []
self._cross_refs = db.get_cross_references(part_id) if self._part else []
# Build manufacturer lookup for country info
try:
mfrs = db.get_manufacturers()
self._manufacturers = {m["id"]: m for m in mfrs}
except Exception:
self._manufacturers = {}
# Set initial column offset to show the selected alternative
selected = context.get("selected_alt_index", 0)
if 0 <= selected < len(self._alternatives):
self._selected_alt = selected
else:
self._selected_alt = 0
self._col_offset = 0
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
@staticmethod
def _format_warranty(months):
"""Format warranty months as 'X meses' or '──' if missing."""
if months is None:
return "\u2500\u2500"
return f"{months} meses"
@staticmethod
def _calc_savings(oem_price, alt_price):
"""Calculate percentage savings of alt vs OEM.
Returns a formatted string like '-28%' or '──' when prices are
unavailable.
"""
if oem_price is None or alt_price is None or oem_price == 0:
return "\u2500\u2500"
pct = ((oem_price - alt_price) / oem_price) * 100
if pct > 0:
return f"-{pct:.0f}%"
elif pct < 0:
return f"+{abs(pct):.0f}%"
return "0%"
@staticmethod
def _format_stock(in_stock):
"""Format boolean in_stock as Si/No."""
if in_stock is None:
return "\u2500\u2500"
return "Si" if in_stock else "No"
def _build_columns(self):
"""Build the column list for draw_comparison.
First column is the OEM part, followed by each aftermarket
alternative. Returns a list of dicts with 'header' and 'rows'.
"""
if self._part is None:
return []
p = self._part
oem_price = None # OEM parts don't have price_usd in our schema
# ── OEM column ──
oem_col = {
"header": "OEM",
"rows": [
("Numero", p.get("oem_part_number", "")),
("Calidad", quality_bar("oem")),
("Tier", "OEM"),
("Precio USD", "\u2500\u2500"),
("Ahorro", "\u2500\u2500"),
("Garantia", "\u2500\u2500"),
("En stock", "\u2500\u2500"),
("Fabricante", p.get("category_name", "")),
],
}
columns = [oem_col]
# ── Aftermarket columns ──
for alt in self._alternatives:
tier = alt.get("quality_tier", "") or ""
price = alt.get("price_usd")
mfr_id = alt.get("manufacturer_id")
mfr_name = alt.get("manufacturer_name", "")
mfr_country = ""
if mfr_id and mfr_id in self._manufacturers:
mfr_country = self._manufacturers[mfr_id].get("country", "") or ""
alt_col = {
"header": mfr_name,
"rows": [
("Numero", alt.get("part_number", "")),
("Calidad", quality_bar(tier.lower()) if tier else "\u2500\u2500"),
("Tier", tier.capitalize()),
("Precio USD", format_currency(price)),
("Ahorro", self._calc_savings(oem_price, price)),
("Garantia", self._format_warranty(alt.get("warranty_months"))),
("En stock", self._format_stock(alt.get("in_stock"))),
("Fabricante", mfr_country if mfr_country else mfr_name),
],
}
columns.append(alt_col)
return columns
# ------------------------------------------------------------------
# Render
# ------------------------------------------------------------------
def render(self, context, db, renderer):
self._load(context, db)
# Header
renderer.draw_header(
f" {APP_NAME} v{VERSION}",
" COMPARADOR OEM vs AFTERMARKET ",
)
if self._part is None:
renderer.draw_text(5, 4, "Parte no encontrada", "error")
renderer.draw_footer([("ESC", "Atras")])
return
# Build comparison columns
all_columns = self._build_columns()
# Apply horizontal scroll: always show OEM (col 0) + offset slice
if len(all_columns) <= 1:
visible_columns = all_columns
else:
# Determine how many alt columns we can show.
# The renderer will auto-size, but let's allow scrolling
# through alternatives.
h, w = renderer.get_size()
# Rough estimate: label_w ~12, each col ~15-20 chars
# We keep it simple: show OEM + up to 3 alternatives at a time
max_visible_alts = max((w - 20) // 18, 1)
alt_cols = all_columns[1:] # all aftermarket columns
end = min(self._col_offset + max_visible_alts, len(alt_cols))
visible_columns = [all_columns[0]] + alt_cols[self._col_offset:end]
part_name = self._part.get("name_es") or self._part.get("name", "")
oem_number = self._part.get("oem_part_number", "")
title = f"COMPARACION: {oem_number} - {part_name}"
renderer.draw_comparison(visible_columns, title=title)
# ── Cross-references below the comparison ──
h, w = renderer.get_size()
# Estimate row where comparison ends:
# title(3) + header(1) + sep(1) + 8 data rows + 1 gap = 14
xref_row = 3 + 3 + 1 + 8 + 2
if self._cross_refs and xref_row < h - 4:
section_title = (
"\u2500\u2500 CROSS-REFERENCES "
+ "\u2500" * max(w - 24, 4)
)
renderer.draw_text(xref_row, 2, section_title, "title")
xref_row += 1
# Group by reference type
by_type = {}
for xr in self._cross_refs:
rtype = xr.get("reference_type", "other") or "other"
by_type.setdefault(rtype, []).append(
xr.get("cross_reference_number", "")
)
for rtype, numbers in by_type.items():
if xref_row >= h - 3:
break
line = f"{rtype.capitalize()}: {', '.join(numbers)}"
renderer.draw_text(xref_row, 4, line, "normal")
xref_row += 1
# Scroll indicator
if len(all_columns) > 1:
alt_count = len(all_columns) - 1
indicator = (
f" Mostrando alternativas "
f"{self._col_offset + 1}-"
f"{min(self._col_offset + len(visible_columns) - 1, alt_count)}"
f" de {alt_count}"
)
indicator_row = min(xref_row + 1, h - 4)
if indicator_row > 0:
renderer.draw_text(indicator_row, 2, indicator, "info")
renderer.draw_footer(_FOOTER)
# ------------------------------------------------------------------
# Key handling
# ------------------------------------------------------------------
def on_key(self, key, context, db, renderer, nav):
# ESC: back to part detail
if key == Key.ESCAPE:
return "back"
# Left arrow: scroll columns left
if key == Key.LEFT:
if self._col_offset > 0:
self._col_offset -= 1
return None
# Right arrow: scroll columns right
if key == Key.RIGHT:
max_offset = max(len(self._alternatives) - 1, 0)
if self._col_offset < max_offset:
self._col_offset += 1
return None
# Number keys (1-9): view alternative detail
if 49 <= key <= 57: # '1'..'9'
idx = key - 49 # 0-based
if self._alternatives and 0 <= idx < len(self._alternatives):
part_id = context.get("part_id")
return (
"comparador",
{"part_id": part_id, "selected_alt_index": idx},
"Comparador",
)
return None
# F3: search for another part (go to buscar_parte)
if key == Key.F3:
return ("buscar_parte", {}, "Buscar Parte")
return None