feat(console): add part detail and comparator screens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
242
console/screens/parte_detalle.py
Normal file
242
console/screens/parte_detalle.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
Part detail screen for the AUTOPARTES console application.
|
||||
|
||||
Shows full part information (OEM number, name, group, category, etc.)
|
||||
with a table of aftermarket alternatives. Number keys navigate to
|
||||
the comparator screen; F4 shows cross-references; F6 lists compatible
|
||||
vehicles.
|
||||
"""
|
||||
|
||||
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, truncate, quality_bar
|
||||
|
||||
|
||||
# Footer labels
|
||||
_FOOTER = [
|
||||
("#", "Comparar"),
|
||||
("F4", "Cross-Ref"),
|
||||
("F6", "Vehiculos"),
|
||||
("ESC", "Atras"),
|
||||
]
|
||||
|
||||
|
||||
class ParteDetalleScreen(Screen):
|
||||
"""Detail view for a single OEM part with aftermarket alternatives."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name="parte_detalle", title="Detalle de Parte")
|
||||
self._part = None
|
||||
self._alternatives = []
|
||||
self._selected_alt = 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Data loading
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _load(self, context, db):
|
||||
"""Load part and alternatives from context['part_id']."""
|
||||
part_id = context.get("part_id")
|
||||
if part_id is None:
|
||||
self._part = None
|
||||
self._alternatives = []
|
||||
return
|
||||
self._part = db.get_part(part_id)
|
||||
self._alternatives = db.get_alternatives(part_id) if self._part else []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _format_warranty(months):
|
||||
"""Format warranty months as 'X meses' or '──' if missing."""
|
||||
if months is None:
|
||||
return "──"
|
||||
return f"{months} meses"
|
||||
|
||||
@staticmethod
|
||||
def _format_weight(kg):
|
||||
"""Format weight in kilograms or '──' if missing."""
|
||||
if kg is None:
|
||||
return "──"
|
||||
return f"{kg} kg"
|
||||
|
||||
@staticmethod
|
||||
def _format_discontinued(flag):
|
||||
"""Format the is_discontinued flag as Si/No."""
|
||||
if flag:
|
||||
return "Si"
|
||||
return "No"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Render
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def render(self, context, db, renderer):
|
||||
self._load(context, db)
|
||||
|
||||
# Header
|
||||
renderer.draw_header(
|
||||
f" {APP_NAME} v{VERSION}",
|
||||
" DETALLE DE PARTE ",
|
||||
)
|
||||
|
||||
if self._part is None:
|
||||
renderer.draw_text(5, 4, "Parte no encontrada", "error")
|
||||
renderer.draw_footer([("ESC", "Atras")])
|
||||
return
|
||||
|
||||
p = self._part
|
||||
|
||||
# ── Top section: part detail fields ──
|
||||
fields = [
|
||||
("Numero OEM", p.get("oem_part_number", "")),
|
||||
("Nombre", p.get("name", "")),
|
||||
("Nombre (ES)", p.get("name_es", "") or ""),
|
||||
("Grupo", p.get("group_name_es") or p.get("group_name", "")),
|
||||
("Categoria", p.get("category_name_es") or p.get("category_name", "")),
|
||||
("Descripcion", p.get("description_es") or p.get("description", "") or ""),
|
||||
("Material", p.get("material", "") or "──"),
|
||||
("Peso", self._format_weight(p.get("weight_kg"))),
|
||||
("Descontinuada", self._format_discontinued(p.get("is_discontinued"))),
|
||||
]
|
||||
renderer.draw_detail(fields, title="INFORMACION DE LA PARTE")
|
||||
|
||||
# ── Bottom section: alternatives table ──
|
||||
h, w = renderer.get_size()
|
||||
# Calculate where the detail section ends (title=3 rows + fields + 1 gap)
|
||||
table_start_row = 3 + 3 + len(fields) + 1
|
||||
|
||||
if self._alternatives:
|
||||
# Draw section title
|
||||
section_title = "\u2500\u2500 ALTERNATIVAS AFTERMARKET " + "\u2500" * max(w - 32, 4)
|
||||
renderer.draw_text(table_start_row, 2, section_title, "title")
|
||||
table_start_row += 1
|
||||
|
||||
headers = ["FABRICANTE", "NUMERO", "CALIDAD", "PRECIO", "GARANTIA"]
|
||||
widths = [14, 16, 10, 10, 10]
|
||||
rows = []
|
||||
for alt in self._alternatives:
|
||||
rows.append((
|
||||
truncate(alt.get("manufacturer_name", ""), 14),
|
||||
truncate(alt.get("part_number", ""), 16),
|
||||
(alt.get("quality_tier", "") or "").capitalize(),
|
||||
format_currency(alt.get("price_usd")),
|
||||
self._format_warranty(alt.get("warranty_months")),
|
||||
))
|
||||
|
||||
renderer.draw_table(
|
||||
headers,
|
||||
rows,
|
||||
widths,
|
||||
selected_row=self._selected_alt,
|
||||
)
|
||||
else:
|
||||
renderer.draw_text(
|
||||
table_start_row, 4,
|
||||
"No hay alternativas aftermarket registradas",
|
||||
"info",
|
||||
)
|
||||
|
||||
renderer.draw_footer(_FOOTER)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Key handling
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def on_key(self, key, context, db, renderer, nav):
|
||||
# ESC: go back
|
||||
if key == Key.ESCAPE:
|
||||
return "back"
|
||||
|
||||
# Arrow navigation for alternatives
|
||||
if key == Key.UP:
|
||||
if self._selected_alt > 0:
|
||||
self._selected_alt -= 1
|
||||
return None
|
||||
|
||||
if key == Key.DOWN:
|
||||
if self._alternatives and self._selected_alt < len(self._alternatives) - 1:
|
||||
self._selected_alt += 1
|
||||
return None
|
||||
|
||||
# Number keys (1-9): navigate to comparador for the selected alternative
|
||||
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
|
||||
|
||||
# ENTER: navigate to comparador for the currently highlighted alternative
|
||||
if key == Key.ENTER:
|
||||
if self._alternatives and 0 <= self._selected_alt < len(self._alternatives):
|
||||
part_id = context.get("part_id")
|
||||
return (
|
||||
"comparador",
|
||||
{"part_id": part_id, "selected_alt_index": self._selected_alt},
|
||||
"Comparador",
|
||||
)
|
||||
return None
|
||||
|
||||
# F4: show cross-references
|
||||
if key == Key.F4:
|
||||
part_id = context.get("part_id")
|
||||
if part_id is None:
|
||||
return None
|
||||
xrefs = db.get_cross_references(part_id)
|
||||
if not xrefs:
|
||||
renderer.show_message("No hay cross-references para esta parte", "info")
|
||||
return None
|
||||
# Build message text grouped by reference type
|
||||
lines = []
|
||||
by_type = {}
|
||||
for xr in xrefs:
|
||||
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():
|
||||
lines.append(f"{rtype.capitalize()}: {', '.join(numbers)}")
|
||||
msg = "CROSS-REFERENCES\n" + "\n".join(lines)
|
||||
renderer.show_message(msg, "info")
|
||||
return None
|
||||
|
||||
# F6: show vehicles that use this part
|
||||
if key == Key.F6:
|
||||
part_id = context.get("part_id")
|
||||
if part_id is None:
|
||||
return None
|
||||
vehicles = db.get_vehicles_for_part(part_id)
|
||||
if not vehicles:
|
||||
renderer.show_message(
|
||||
"No hay vehiculos registrados para esta parte", "info"
|
||||
)
|
||||
return None
|
||||
# Build message with vehicle list (limit to avoid overflow)
|
||||
lines = []
|
||||
for v in vehicles[:10]:
|
||||
brand = v.get("brand", "")
|
||||
model = v.get("model", "")
|
||||
year = v.get("year", "")
|
||||
engine = v.get("engine", "")
|
||||
line = f"{brand} {model} {year}"
|
||||
if engine:
|
||||
line += f" ({engine})"
|
||||
position = v.get("position", "")
|
||||
if position:
|
||||
line += f" - {position}"
|
||||
lines.append(line)
|
||||
if len(vehicles) > 10:
|
||||
lines.append(f"... y {len(vehicles) - 10} mas")
|
||||
msg = "VEHICULOS COMPATIBLES\n" + "\n".join(lines)
|
||||
renderer.show_message(msg, "info")
|
||||
return None
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user