""" Admin CRUD screen for Parts in the NEXUS AUTOPARTS console application. Provides a paginated list view with create (F3), edit (ENTER), and delete (F8/Del) operations. Form editing is handled inline with field-by-field navigation. """ 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, pad_right # Form field definitions for create/edit _FIELDS = [ {'label': 'Numero OEM', 'key': 'oem_part_number', 'width': 20}, {'label': 'Nombre', 'key': 'name', 'width': 40}, {'label': 'Nombre (ES)', 'key': 'name_es', 'width': 40}, {'label': 'Grupo ID', 'key': 'group_id', 'width': 5, 'hint': 'F1=Lista'}, {'label': 'Descripcion', 'key': 'description', 'width': 50}, {'label': 'Descripcion ES', 'key': 'description_es', 'width': 50}, {'label': 'Material', 'key': 'material', 'width': 20}, {'label': 'Peso (kg)', 'key': 'weight_kg', 'width': 8}, {'label': 'Descontinuada', 'key': 'is_discontinued', 'width': 1, 'hint': 'S/N'}, ] # Footer labels per mode _FOOTER_LIST = [ ("F3", "Nuevo"), ("ENTER", "Editar"), ("F8", "Eliminar"), ("PgUp/Dn", "Paginar"), ("ESC", "Atras"), ] _FOOTER_FORM = [ ("TAB/Down", "Siguiente"), ("Up", "Anterior"), ("F9", "Guardar"), ("ESC", "Cancelar"), ] class AdminPartesScreen(Screen): """Admin CRUD screen for the parts table.""" def __init__(self): super().__init__(name="admin_partes", title="Administracion de Partes") self._mode = 'list' # 'list' or 'form' self._page = 1 self._per_page = 15 self._selected = 0 self._parts = [] self._editing_id = None # None = creating, int = editing self._focused_field = 0 self._form_data = {} self._dirty = False # ------------------------------------------------------------------ # Data loading # ------------------------------------------------------------------ def _load_parts(self, db): """Load the current page of parts.""" self._parts = db.get_parts(page=self._page, per_page=self._per_page) def _init_form(self, part=None): """Initialise form_data from an existing part or blank.""" self._form_data = {} if part: for f in _FIELDS: key = f['key'] val = part.get(key, '') if key == 'is_discontinued': val = 'S' if val else 'N' self._form_data[key] = str(val) if val is not None else '' else: for f in _FIELDS: self._form_data[f['key']] = '' self._focused_field = 0 self._dirty = False # ------------------------------------------------------------------ # Render # ------------------------------------------------------------------ def render(self, context, db, renderer): renderer.draw_header( f" {APP_NAME} v{VERSION}", " ADMINISTRACION DE PARTES ", ) if self._mode == 'list': self._render_list(db, renderer) else: self._render_form(renderer) def _render_list(self, db, renderer): """Render the paginated parts list.""" self._load_parts(db) headers = ["NUMERO OEM", "NOMBRE", "GRUPO", "MATERIAL", "DISCONT"] widths = [18, 25, 15, 12, 7] rows = [] for p in self._parts: disc = "Si" if p.get("is_discontinued") else "" rows.append(( truncate(p.get("oem_part_number", ""), 18), truncate(p.get("name_es") or p.get("name", ""), 25), truncate(p.get("group_name", ""), 15), truncate(p.get("material", "") or "", 12), disc, )) 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_LIST) def _render_form(self, renderer): """Render the create/edit form.""" title = "EDITAR PARTE" if self._editing_id else "NUEVA PARTE" fields = [] for f in _FIELDS: fields.append({ 'label': f['label'], 'value': self._form_data.get(f['key'], ''), 'width': f['width'], 'hint': f.get('hint', ''), }) renderer.draw_form(fields, focused_index=self._focused_field, title=title) renderer.draw_footer(_FOOTER_FORM) # ------------------------------------------------------------------ # Key handling # ------------------------------------------------------------------ def on_key(self, key, context, db, renderer, nav): if self._mode == 'list': return self._handle_list_key(key, context, db, renderer) else: return self._handle_form_key(key, db, renderer) def _handle_list_key(self, key, context, db, renderer): """Handle keys in list mode.""" # ESC: go back if key == Key.ESCAPE: return "back" # Arrow navigation if key == Key.UP: if self._selected > 0: self._selected -= 1 return None if key == Key.DOWN: if self._parts and self._selected < len(self._parts) - 1: self._selected += 1 return None # PgDn: next page if key == Key.PGDN: if len(self._parts) == self._per_page: self._page += 1 self._selected = 0 return None # PgUp: previous page if key == Key.PGUP: if self._page > 1: self._page -= 1 self._selected = 0 return None # F3: create new part if key == Key.F3: self._mode = 'form' self._editing_id = None self._init_form() return None # ENTER: edit selected part if key == Key.ENTER: if self._parts and 0 <= self._selected < len(self._parts): part_row = self._parts[self._selected] part = db.get_part(part_row["id"]) if part: self._editing_id = part["id"] self._mode = 'form' self._init_form(part) return None # Number keys 1-9: edit part at that row index if 49 <= key <= 57: idx = key - 49 if 0 <= idx < len(self._parts): part_row = self._parts[idx] part = db.get_part(part_row["id"]) if part: self._editing_id = part["id"] self._mode = 'form' self._init_form(part) return None # F8 or DEL: delete selected part if key in (Key.F8, 330): # 330 = KEY_DC (Delete) if self._parts and 0 <= self._selected < len(self._parts): part = self._parts[self._selected] name = part.get("name_es") or part.get("name", "") oem = part.get("oem_part_number", "") confirmed = renderer.show_message( f"Eliminar parte?\n{oem} - {name}", "confirm", ) if confirmed: db.delete_part(part["id"]) # Adjust selection if self._selected >= len(self._parts) - 1: self._selected = max(0, self._selected - 1) return None return None def _handle_form_key(self, key, db, renderer): """Handle keys in form mode.""" # ESC: cancel form (with dirty check) if key == Key.ESCAPE: if self._dirty: confirmed = renderer.show_message( "Descartar cambios?", "confirm" ) if not confirmed: return None self._mode = 'list' return None # TAB / Down: next field if key in (Key.TAB, Key.DOWN): if self._focused_field < len(_FIELDS) - 1: self._focused_field += 1 return None # Up: previous field if key == Key.UP: if self._focused_field > 0: self._focused_field -= 1 return None # F9: save if key == Key.F9: return self._save(db, renderer) # Backspace: delete last char from current field value if key in (Key.BACKSPACE, 8): field_key = _FIELDS[self._focused_field]['key'] val = self._form_data.get(field_key, '') if val: self._form_data[field_key] = val[:-1] self._dirty = True return None # Printable characters: append to current field if 32 <= key <= 126: field_def = _FIELDS[self._focused_field] field_key = field_def['key'] val = self._form_data.get(field_key, '') if len(val) < field_def['width']: self._form_data[field_key] = val + chr(key) self._dirty = True return None return None def _save(self, db, renderer): """Validate and save the form data.""" data = dict(self._form_data) # Validate required fields if not data.get('oem_part_number', '').strip(): renderer.show_message("Numero OEM es requerido", "error") return None if not data.get('name', '').strip(): renderer.show_message("Nombre es requerido", "error") return None # Convert types gid = data.get('group_id', '').strip() data['group_id'] = int(gid) if gid.isdigit() else None wkg = data.get('weight_kg', '').strip() try: data['weight_kg'] = float(wkg) if wkg else None except ValueError: data['weight_kg'] = None disc = data.get('is_discontinued', '').strip().upper() data['is_discontinued'] = 1 if disc == 'S' else 0 try: if self._editing_id: db.update_part(self._editing_id, data) renderer.show_message("Parte actualizada correctamente", "info") else: db.create_part(data) renderer.show_message("Parte creada correctamente", "info") except Exception as exc: renderer.show_message(f"Error al guardar:\n{exc}", "error") return None self._mode = 'list' self._dirty = False return None