diff --git a/console/screens/vehiculo_nav.py b/console/screens/vehiculo_nav.py new file mode 100644 index 0000000..b46bea8 --- /dev/null +++ b/console/screens/vehiculo_nav.py @@ -0,0 +1,239 @@ +""" +Vehicle drill-down navigation screen for the AUTOPARTES console application. + +Guides the user through a four-level hierarchy: + + Brand -> Model -> Year -> Engine + +Each level presents a filterable list. After engine selection the screen +navigates to the catalogue (``catalogo``) with the resolved +``model_year_engine`` id. +""" + +from console.core.screens import Screen +from console.core.keybindings import Key + +# Ordered sequence of drill-down levels. +_LEVELS = ("brand", "model", "year", "engine") + +# Human-readable titles for each level (Spanish). +_LEVEL_TITLES = { + "brand": "Seleccione Marca", + "model": "Seleccione Modelo", + "year": "Seleccione Ano", + "engine": "Seleccione Motor", +} + + +class VehiculoNavScreen(Screen): + """Four-level vehicle drill-down: Brand -> Model -> Year -> Engine.""" + + def __init__(self): + super().__init__("vehiculo_nav", "Consulta por Vehiculo") + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _get_title_for_level(self, context): + """Return the title string for the current drill-down level.""" + level = context.get("level", "brand") + return _LEVEL_TITLES.get(level, "Seleccione") + + def _get_subtitle(self, context): + """Build a breadcrumb subtitle from selections made so far. + + Example: ``"TOYOTA > CAMRY > 2023 > Seleccione motor"`` + """ + parts = [] + if context.get("brand"): + parts.append(context["brand"]) + if context.get("model"): + parts.append(context["model"]) + if context.get("year") is not None: + parts.append(str(context["year"])) + + level = context.get("level", "brand") + parts.append(_LEVEL_TITLES.get(level, "")) + return " > ".join(parts) + + def _load_items(self, context, db): + """Fetch the item list from the database for the current level.""" + level = context.get("level", "brand") + + if level == "brand": + context["all_items"] = db.get_brands() + elif level == "model": + context["all_items"] = db.get_models(brand=context.get("brand")) + elif level == "year": + context["all_items"] = db.get_years( + brand=context.get("brand"), + model=context.get("model"), + ) + elif level == "engine": + context["all_items"] = db.get_engines( + brand=context.get("brand"), + model=context.get("model"), + year=context.get("year"), + ) + else: + context["all_items"] = [] + + self._apply_filter(context) + + def _apply_filter(self, context): + """Reduce ``all_items`` to those matching ``filter_text``. + + Matching is a case-insensitive substring test on the display name. + """ + level = context.get("level", "brand") + ft = context.get("filter_text", "").lower() + all_items = context.get("all_items", []) + + if ft: + context["filtered_items"] = [ + item for item in all_items + if ft in self._get_display_name(item, level).lower() + ] + else: + context["filtered_items"] = list(all_items) + + @staticmethod + def _get_display_name(item, level): + """Extract the human-readable display string from an item dict.""" + if level == "year": + return str(item.get("year", "")) + return item.get("name", "") + + # ------------------------------------------------------------------ + # Screen interface + # ------------------------------------------------------------------ + + def render(self, context, db, renderer): + # First-render initialisation + if "level" not in context: + context["level"] = "brand" + context["filter_text"] = "" + context["selected_index"] = 0 + self._load_items(context, db) + + level = context["level"] + title = self._get_title_for_level(context) + subtitle = self._get_subtitle(context) + renderer.draw_header(title, subtitle) + + # Build the (number, label) tuples expected by draw_filter_list. + filtered = context.get("filtered_items", []) + display_items = [ + (str(idx + 1), self._get_display_name(item, level)) + for idx, item in enumerate(filtered) + ] + + renderer.draw_filter_list( + display_items, + context.get("filter_text", ""), + context.get("selected_index", 0), + title=f"SELECCIONAR {level.upper()}", + ) + + renderer.draw_footer([ + ("Escriba", "Filtrar"), + ("ENTER", "Seleccionar"), + ("\u2191\u2193", "Mover"), + ("ESC", "Atras"), + ]) + + def on_key(self, key, context, db, renderer, nav): + filtered = context.get("filtered_items", []) + level = context.get("level", "brand") + + # -- ESC: go back one level, or return to menu ---------------- + if key == Key.ESCAPE: + if level == "brand": + return "back" + prev = _LEVELS[_LEVELS.index(level) - 1] + context["level"] = prev + context["filter_text"] = "" + context["selected_index"] = 0 + self._load_items(context, db) + return None + + # -- ENTER: select item and advance --------------------------- + if key == Key.ENTER and filtered: + idx = context.get("selected_index", 0) + if idx >= len(filtered): + return None + selected = filtered[idx] + + if level == "brand": + context["brand"] = selected["name"] + context["level"] = "model" + elif level == "model": + context["model"] = selected["name"] + context["level"] = "year" + elif level == "year": + context["year"] = selected["year"] + context["level"] = "engine" + elif level == "engine": + context["engine_id"] = selected["id"] + context["engine_name"] = selected["name"] + # Resolve the model_year_engine row + mye_list = db.get_model_year_engine( + context["brand"], + context["model"], + context["year"], + context["engine_id"], + ) + if mye_list: + mye_id = mye_list[0]["id"] + return ( + "catalogo", + { + "mye_id": mye_id, + "brand": context["brand"], + "model": context["model"], + "year": context["year"], + "engine": context["engine_name"], + }, + f"{context['brand']} {context['model']} {context['year']}", + ) + else: + renderer.show_message( + "No se encontro configuracion para este vehiculo", + "error", + ) + return None + + # Reset filter for the new level + context["filter_text"] = "" + context["selected_index"] = 0 + self._load_items(context, db) + return None + + # -- Arrow keys: move selection cursor ------------------------ + if key == Key.UP: + if context.get("selected_index", 0) > 0: + context["selected_index"] -= 1 + return None + + if key == Key.DOWN: + if context.get("selected_index", 0) < len(filtered) - 1: + context["selected_index"] += 1 + return None + + # -- Backspace: trim filter text ------------------------------ + if key in (Key.BACKSPACE, 8, 263): + if context.get("filter_text"): + context["filter_text"] = context["filter_text"][:-1] + self._apply_filter(context) + context["selected_index"] = 0 + return None + + # -- Printable characters: append to filter ------------------- + if isinstance(key, int) and 32 <= key <= 126: + context["filter_text"] = context.get("filter_text", "") + chr(key) + self._apply_filter(context) + context["selected_index"] = 0 + return None + + return None