- Migrate from SQLite to PostgreSQL with normalized schema - Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission, materials, position_part, manufacture_type, quality_tier, countries, reference_type, shapes) - Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries - Rewrite console/db.py (27 methods) using SQLAlchemy ORM - Add models.py with 27 SQLAlchemy model definitions - Add config.py for centralized DB_URL configuration - Add migrate_to_postgres.py migration script - Add docs/METABASE_GUIDE.md with complete data entry guide - Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS" - Fill vehicle data gaps via NHTSA API + heuristics: engines (cylinders, power, torque), brands (country, founded_year), models (body_type, production years), MYE (drivetrain, transmission, trim) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
5.2 KiB
Python
175 lines
5.2 KiB
Python
"""
|
|
Full-text search screen for the NEXUS AUTOPARTS console application.
|
|
|
|
Prompts the user for a search query and displays matching parts using
|
|
the FTS5 full-text search engine (with LIKE fallback). Results are
|
|
paginated and selecting a row navigates to the part detail screen.
|
|
"""
|
|
|
|
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
|
|
_FOOTER_INPUT = [
|
|
("ENTER", "Buscar"),
|
|
("ESC", "Atras"),
|
|
]
|
|
|
|
_FOOTER_RESULTS = [
|
|
("1-9", "Ver parte"),
|
|
("ENTER", "Ver parte"),
|
|
("PgUp/Dn", "Paginar"),
|
|
("F3", "Nueva busqueda"),
|
|
("ESC", "Atras"),
|
|
]
|
|
|
|
|
|
class BuscarTextoScreen(Screen):
|
|
"""Full-text search by description / name."""
|
|
|
|
def __init__(self):
|
|
super().__init__(name="buscar_texto", title="Buscar por Descripcion")
|
|
self._results = None
|
|
self._search_term = None
|
|
self._selected = 0
|
|
self._page = 1
|
|
self._per_page = 15
|
|
self._needs_input = True
|
|
|
|
# ------------------------------------------------------------------
|
|
# Render
|
|
# ------------------------------------------------------------------
|
|
|
|
def render(self, context, db, renderer):
|
|
# Header
|
|
renderer.draw_header(
|
|
f" {APP_NAME} v{VERSION}",
|
|
" BUSCAR POR DESCRIPCION ",
|
|
)
|
|
|
|
if self._needs_input:
|
|
renderer.draw_footer(_FOOTER_INPUT)
|
|
return
|
|
|
|
if self._results is None:
|
|
renderer.draw_text(5, 4, "Presione F3 para buscar", "info")
|
|
renderer.draw_footer(_FOOTER_RESULTS)
|
|
return
|
|
|
|
if not self._results:
|
|
renderer.draw_text(
|
|
5, 4,
|
|
f'No se encontraron resultados para "{self._search_term}"',
|
|
"info",
|
|
)
|
|
renderer.draw_footer(_FOOTER_RESULTS)
|
|
return
|
|
|
|
# Display results table
|
|
headers = ["NUMERO OEM", "NOMBRE", "CATEGORIA", "GRUPO"]
|
|
widths = [18, 28, 18, 18]
|
|
rows = []
|
|
for r in self._results:
|
|
rows.append((
|
|
truncate(r.get("oem_part_number", ""), 18),
|
|
truncate(r.get("name_es") or r.get("name", ""), 28),
|
|
truncate(r.get("category_name", ""), 18),
|
|
truncate(r.get("group_name", ""), 18),
|
|
))
|
|
|
|
renderer.draw_table(
|
|
headers,
|
|
rows,
|
|
widths,
|
|
page_info={
|
|
"page": self._page,
|
|
"total_pages": self._page,
|
|
"total_rows": len(rows),
|
|
},
|
|
selected_row=self._selected,
|
|
)
|
|
renderer.draw_footer(_FOOTER_RESULTS)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Key handling
|
|
# ------------------------------------------------------------------
|
|
|
|
def _do_search(self, db):
|
|
"""Execute the full-text search with current parameters."""
|
|
self._results = db.search_parts(
|
|
self._search_term,
|
|
page=self._page,
|
|
per_page=self._per_page,
|
|
)
|
|
self._selected = 0
|
|
|
|
def on_key(self, key, context, db, renderer, nav):
|
|
# If we need input, show the input dialog
|
|
if self._needs_input:
|
|
self._needs_input = False
|
|
value = renderer.show_input("Buscar", max_len=40)
|
|
if value is None:
|
|
# User pressed ESC in input dialog
|
|
if self._results is not None:
|
|
return None
|
|
return "back"
|
|
if value.strip():
|
|
self._search_term = value.strip()
|
|
self._page = 1
|
|
self._do_search(db)
|
|
return None
|
|
|
|
# ESC: go back
|
|
if key == Key.ESCAPE:
|
|
return "back"
|
|
|
|
# F3: new search
|
|
if key == Key.F3:
|
|
self._needs_input = True
|
|
return None
|
|
|
|
# Arrow navigation
|
|
if key == Key.UP:
|
|
if self._selected > 0:
|
|
self._selected -= 1
|
|
return None
|
|
|
|
if key == Key.DOWN:
|
|
if self._results and self._selected < len(self._results) - 1:
|
|
self._selected += 1
|
|
return None
|
|
|
|
# ENTER: view selected part
|
|
if key == Key.ENTER:
|
|
if self._results and 0 <= self._selected < len(self._results):
|
|
part = self._results[self._selected]
|
|
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 self._results and 0 <= idx < len(self._results):
|
|
part = self._results[idx]
|
|
return ("parte_detalle", {"part_id": part["id"]}, "Parte")
|
|
return None
|
|
|
|
# PgDn: next page
|
|
if key == Key.PGDN:
|
|
if self._results and len(self._results) >= self._per_page:
|
|
self._page += 1
|
|
self._do_search(db)
|
|
return None
|
|
|
|
# PgUp: previous page
|
|
if key == Key.PGUP:
|
|
if self._page > 1:
|
|
self._page -= 1
|
|
self._do_search(db)
|
|
return None
|
|
|
|
return None
|