""" Part comparator screen for the NEXUS AUTOPARTS 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