Files
Autoparts-DB/console/screens/admin_partes.py

322 lines
11 KiB
Python

"""
Admin CRUD screen for Parts in the AUTOPARTES 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