Files
Autoparts-DB/console/screens/catalogo.py
2026-02-15 01:49:15 +00:00

355 lines
12 KiB
Python

"""
Catalog navigation screen for the AUTOPARTES console application.
Provides a three-level drill-down through the parts hierarchy:
Categories -> Groups -> Parts. An optional vehicle filter (mye_id)
restricts the parts list to those that fit a specific vehicle
configuration.
"""
from console.core.screens import Screen
from console.core.keybindings import Key
from console.config import APP_NAME, VERSION
from console.utils.formatting import truncate
# Footer labels for each navigation level
_FOOTER_CATEGORIES = [
("1-9", "Seleccionar"),
("Filtro", "Teclear"),
("F10", "Menu"),
("ESC", "Atras"),
]
_FOOTER_GROUPS = [
("1-9", "Seleccionar"),
("Filtro", "Teclear"),
("F10", "Menu"),
("ESC", "Atras"),
]
_FOOTER_PARTS = [
("1-9", "Ver parte"),
("ENTER", "Ver parte"),
("PgUp/Dn", "Paginar"),
("ESC", "Atras"),
]
class CatalogoScreen(Screen):
"""Hierarchical catalog browser: Categories -> Groups -> Parts."""
def __init__(self):
super().__init__(name="catalogo", title="Catalogo")
self._filter_text = ""
self._selected = 0
self._items = []
self._parts_data = []
self._selected_part = 0
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _ensure_defaults(self, context):
"""Set default context values if missing."""
context.setdefault("level", "categories")
context.setdefault("mye_id", None)
context.setdefault("brand", "")
context.setdefault("model", "")
context.setdefault("year", "")
context.setdefault("engine", "")
context.setdefault("category_id", None)
context.setdefault("category_name", "")
context.setdefault("group_id", None)
context.setdefault("group_name", "")
context.setdefault("page", 1)
context.setdefault("per_page", 15)
def _build_header_title(self, context):
"""Build the header title based on context."""
level = context["level"]
parts = []
# Vehicle info if available
if context.get("brand"):
vehicle = " ".join(
filter(None, [
context["brand"],
context["model"],
str(context["year"]) if context["year"] else "",
])
)
parts.append(vehicle)
if level == "categories":
parts.append("Categorias")
elif level == "groups":
parts.append(context.get("category_name", "Grupos"))
elif level == "parts":
cat = context.get("category_name", "")
grp = context.get("group_name", "")
if cat and grp:
parts.append(f"{cat} > {grp}")
elif grp:
parts.append(grp)
return "".join(parts) if parts else "CATALOGO DE CATEGORIAS"
def _load_categories(self, db):
"""Load and filter categories."""
categories = db.get_categories()
if self._filter_text:
ft = self._filter_text.upper()
categories = [
c for c in categories
if ft in (c.get("name_es") or c.get("name") or "").upper()
or ft in (c.get("name") or "").upper()
]
self._items = [
(str(i + 1), c.get("name_es") or c.get("name", ""), c["id"])
for i, c in enumerate(categories)
]
def _load_groups(self, db, category_id):
"""Load and filter groups for a category."""
groups = db.get_groups(category_id)
if self._filter_text:
ft = self._filter_text.upper()
groups = [
g for g in groups
if ft in (g.get("name_es") or g.get("name") or "").upper()
or ft in (g.get("name") or "").upper()
]
self._items = [
(str(i + 1), g.get("name_es") or g.get("name", ""), g["id"])
for i, g in enumerate(groups)
]
def _load_parts(self, db, context):
"""Load parts for the current group/vehicle with pagination."""
self._parts_data = db.get_parts(
group_id=context.get("group_id"),
mye_id=context.get("mye_id"),
page=context.get("page", 1),
per_page=context.get("per_page", 15),
)
def _reset_filter(self):
"""Reset filter text and selection."""
self._filter_text = ""
self._selected = 0
# ------------------------------------------------------------------
# Render
# ------------------------------------------------------------------
def render(self, context, db, renderer):
self._ensure_defaults(context)
level = context["level"]
# Header
header_title = self._build_header_title(context)
renderer.draw_header(
f" {APP_NAME} v{VERSION}",
f" {header_title} ",
)
if level == "categories":
self._load_categories(db)
display_items = [(num, label) for num, label, _id in self._items]
renderer.draw_filter_list(
display_items,
self._filter_text,
self._selected,
title="CATALOGO DE CATEGORIAS",
)
renderer.draw_footer(_FOOTER_CATEGORIES)
elif level == "groups":
self._load_groups(db, context["category_id"])
display_items = [(num, label) for num, label, _id in self._items]
renderer.draw_filter_list(
display_items,
self._filter_text,
self._selected,
title=context.get("category_name", "GRUPOS"),
)
renderer.draw_footer(_FOOTER_GROUPS)
elif level == "parts":
self._load_parts(db, context)
headers = ["NUMERO OEM", "DESCRIPCION", "GRUPO", "ALT"]
widths = [18, 30, 18, 5]
rows = []
for p in self._parts_data:
alts = len(db.get_alternatives(p["id"]))
rows.append((
truncate(p.get("oem_part_number", ""), 18),
truncate(
p.get("name_es") or p.get("name", ""), 30
),
truncate(p.get("group_name", ""), 18),
str(alts) if alts > 0 else "",
))
page = context.get("page", 1)
renderer.draw_table(
headers,
rows,
widths,
page_info={"page": page, "total_pages": page, "total_rows": len(rows)},
selected_row=self._selected_part,
)
renderer.draw_footer(_FOOTER_PARTS)
# ------------------------------------------------------------------
# Key handling
# ------------------------------------------------------------------
def on_key(self, key, context, db, renderer, nav):
self._ensure_defaults(context)
level = context["level"]
if level in ("categories", "groups"):
return self._handle_filter_level(key, context)
elif level == "parts":
return self._handle_parts_level(key, context)
return None
def _handle_filter_level(self, key, context):
"""Handle keys for categories and groups levels (filter list)."""
level = context["level"]
# ESC: go back
if key == Key.ESCAPE:
if level == "groups":
context["level"] = "categories"
context["category_id"] = None
context["category_name"] = ""
self._reset_filter()
return None
return "back"
# Arrow navigation
if key == Key.UP:
if self._selected > 0:
self._selected -= 1
return None
if key == Key.DOWN:
if self._items and self._selected < len(self._items) - 1:
self._selected += 1
return None
# ENTER: select current item
if key == Key.ENTER:
if self._items and 0 <= self._selected < len(self._items):
return self._select_item(context, self._selected)
return None
# Number keys: direct selection (1-9)
if 49 <= key <= 57: # '1'..'9'
idx = key - 49 # 0-based
if 0 <= idx < len(self._items):
return self._select_item(context, idx)
return None
# Backspace: remove last filter character
if key in (Key.BACKSPACE, 8):
if self._filter_text:
self._filter_text = self._filter_text[:-1]
self._selected = 0
elif context["level"] == "groups":
context["level"] = "categories"
context["category_id"] = None
context["category_name"] = ""
self._reset_filter()
else:
return "back"
return None
# Printable characters: add to filter
if 32 <= key <= 126:
self._filter_text += chr(key)
self._selected = 0
return None
return None
def _select_item(self, context, idx):
"""Handle selection of an item at the given index."""
_num, label, item_id = self._items[idx]
level = context["level"]
if level == "categories":
context["level"] = "groups"
context["category_id"] = item_id
context["category_name"] = label
self._reset_filter()
return None
elif level == "groups":
context["level"] = "parts"
context["group_id"] = item_id
context["group_name"] = label
context["page"] = 1
self._selected_part = 0
self._reset_filter()
return None
return None
def _handle_parts_level(self, key, context):
"""Handle keys for the parts table level."""
# ESC: go back to groups
if key == Key.ESCAPE:
context["level"] = "groups"
context["group_id"] = None
context["group_name"] = ""
self._selected_part = 0
self._reset_filter()
return None
# Arrow navigation
if key == Key.UP:
if self._selected_part > 0:
self._selected_part -= 1
return None
if key == Key.DOWN:
if self._parts_data and self._selected_part < len(self._parts_data) - 1:
self._selected_part += 1
return None
# ENTER: view selected part detail
if key == Key.ENTER:
if self._parts_data and 0 <= self._selected_part < len(self._parts_data):
part = self._parts_data[self._selected_part]
return ("parte_detalle", {"part_id": part["id"]}, "Parte")
return None
# Number keys: direct selection (1-9)
if 49 <= key <= 57: # '1'..'9'
idx = key - 49 # 0-based
if 0 <= idx < len(self._parts_data):
part = self._parts_data[idx]
return ("parte_detalle", {"part_id": part["id"]}, "Parte")
return None
# PgDn: next page
if key == Key.PGDN:
context["page"] = context.get("page", 1) + 1
self._selected_part = 0
return None
# PgUp: previous page
if key == Key.PGUP:
if context.get("page", 1) > 1:
context["page"] = context["page"] - 1
self._selected_part = 0
return None
return None