feat(console): add vehicle drill-down navigation screen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
239
console/screens/vehiculo_nav.py
Normal file
239
console/screens/vehiculo_nav.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user