zcatgui/docs/ADVANCED_TABLE_DESIGN.md
reugenio 83049a99be feat: AdvancedTable widget - Fases 1-6 IMPLEMENTADO (pendiente aprobacion)
Widget AdvancedTable portado de Go (simifactu-fyne) a Zig.
2,526 LOC en 4 archivos, 370 tests pasan.

Archivos:
- types.zig (369 LOC): CellValue, ColumnType, RowState, TableColors
- schema.zig (373 LOC): ColumnDef, TableSchema, DataStore interface
- state.zig (762 LOC): Selection, editing, dirty tracking, snapshots
- advanced_table.zig (1,022 LOC): Widget, rendering, keyboard

Fases implementadas:
1. Core (types, schema, state)
2. Navigation (arrows, Tab, PgUp/Dn, Home/End, Ctrl+Home/End)
3. Cell Editing (F2/Enter start, Escape cancel, text input)
4. Sorting (header click, visual indicators)
5. Auto-CRUD (CREATE/UPDATE/DELETE detection on row change)
6. Row Operations (Ctrl+N insert, Ctrl+Delete remove)

Fases diferidas (7-8): Lookup & Auto-fill, Callbacks avanzados

ESTADO: Compilado y tests pasan. NO probado en uso real.
REQUIERE: Aprobacion antes de tag de version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 11:25:48 +01:00

582 lines
18 KiB
Markdown

# AdvancedTable - Documento de Diseño
> **Fecha**: 2025-12-17
> **Basado en**: Análisis de `simifactu-fyne/internal/ui/components/advanced_table/` (~2,800 LOC Go)
> **Target**: zcatgui (Zig)
---
## 1. Resumen Ejecutivo
AdvancedTable es una tabla parametrizada de alto nivel que proporciona:
- **Configuración schema-driven** (sin callbacks hardcoded)
- **Edición Excel-style** (Entry overlay sobre celda)
- **Auto-CRUD** (detecta y ejecuta CREATE/UPDATE/DELETE automáticamente)
- **Navegación teclado completa** (arrows, Tab, Enter, Escape)
- **Sorting por columna** (click header, cycle asc/desc/restore)
- **Visual states** (normal, modified, new, deleted, error, selected)
- **Lookup & Auto-fill** (para campos de lookup de BD)
La idea es que cualquier tabla en la aplicación (WHO, líneas de documento, configuración, etc.) sea simplemente una **instancia parametrizada** de AdvancedTable.
---
## 2. Arquitectura Propuesta para zcatgui
### 2.1 Estructura de Archivos
```
src/widgets/advanced_table/
├── advanced_table.zig # Entry point, re-exports
├── types.zig # Tipos, enums, configs
├── schema.zig # TableSchema, ColumnDef
├── state.zig # AdvancedTableState
├── render.zig # Rendering visual
├── editing.zig # Edición in-situ
├── navigation.zig # Navegación teclado
├── sorting.zig # Sorting por columnas
├── row_ops.zig # Operaciones de filas (CRUD local)
├── autocrud.zig # Detección y ejecución Auto-CRUD
├── lookup.zig # Lookup y Auto-fill
└── callbacks.zig # Sistema de callbacks
```
### 2.2 Tipos Fundamentales
```zig
// types.zig
/// Tipos de datos para columnas
pub const ColumnType = enum {
string,
integer,
float,
money,
boolean,
date,
select, // Dropdown con opciones
lookup, // Busca en tabla relacionada
};
/// Estado de fila (dirty tracking)
pub const RowState = enum {
normal, // Sin cambios
modified, // Editado, pendiente de guardar
new, // Fila nueva, no existe en BD
deleted, // Marcado para borrar
error, // Error de validación
};
/// Estado de lock de fila (para VeriFacTu certified docs)
pub const RowLockState = enum {
unlocked, // Editable
locked, // Requiere password
read_only, // Solo lectura (certificado)
};
/// Resultado de validación
pub const ValidationResult = struct {
valid: bool,
message: []const u8 = "",
severity: enum { info, warning, error } = .error,
};
```
### 2.3 Schema de Tabla
```zig
// schema.zig
/// Definición de columna
pub const ColumnDef = struct {
/// Nombre interno (key en row map)
name: []const u8,
/// Título visible en header
title: []const u8,
/// Ancho en pixels
width: u32 = 100,
/// Tipo de datos
column_type: ColumnType = .string,
/// Es editable
editable: bool = true,
/// Es ordenable
sortable: bool = true,
/// Ancho mínimo
min_width: u32 = 40,
/// Alineación
align: enum { left, center, right } = .left,
// Lookup
enable_lookup: bool = false,
lookup_table: ?[]const u8 = null,
lookup_key_column: ?[]const u8 = null,
auto_fill_columns: ?[]const AutoFillMapping = null,
// Callbacks por columna (opcionales)
validator: ?*const fn(value: CellValue) ValidationResult = null,
formatter: ?*const fn(value: CellValue) []const u8 = null,
parser: ?*const fn(text: []const u8) ?CellValue = null,
};
/// Mapping para auto-fill después de lookup
pub const AutoFillMapping = struct {
source_field: []const u8, // Campo en tabla lookup
target_column: []const u8, // Columna destino
};
/// Schema completo de tabla
pub const TableSchema = struct {
/// Nombre de la tabla (para DataStore)
table_name: []const u8,
/// Columnas
columns: []const ColumnDef,
// Configuración visual
show_row_state_indicators: bool = true,
show_headers: bool = true,
header_height: u32 = 28,
row_height: u32 = 24,
alternating_rows: bool = true,
// Configuración de edición
allow_edit: bool = true,
allow_sorting: bool = true,
allow_row_operations: bool = true, // Ctrl+N, Delete, etc.
// Auto-CRUD
auto_crud_enabled: bool = true, // Detecta CREATE/UPDATE/DELETE al cambiar fila
always_show_empty_row: bool = false, // Fila vacía al final para entrada continua
// Row locking (para certificados)
support_row_locking: bool = false,
// DataStore (opcional, para Auto-CRUD)
data_store: ?*DataStore = null,
// Colors (opcional, override del theme)
colors: ?*const TableColors = null,
};
```
### 2.4 State de la Tabla
```zig
// state.zig
pub const AdvancedTableState = struct {
// Datos
rows: std.ArrayList(Row),
allocator: std.mem.Allocator,
// Selección
selected_row: i32 = -1,
selected_col: i32 = -1,
prev_selected_row: i32 = -1,
prev_selected_col: i32 = -1,
// Edición
editing: bool = false,
edit_buffer: [256]u8 = undefined,
edit_len: usize = 0,
edit_cursor: usize = 0,
original_value: ?CellValue = null,
escape_count: u8 = 0, // 1=revert, 2=cancel
// Sorting
sort_column: i32 = -1,
sort_ascending: bool = true,
original_order: ?std.ArrayList(Row) = null, // Para restore
// Scroll
scroll_row: usize = 0,
scroll_x: usize = 0,
// State maps (sparse - solo filas modificadas)
dirty_rows: std.AutoHashMap(usize, bool),
new_rows: std.AutoHashMap(usize, bool),
deleted_rows: std.AutoHashMap(usize, bool),
validation_errors: std.AutoHashMap(usize, bool),
// Snapshots para Auto-CRUD
row_snapshots: std.AutoHashMap(usize, Row),
// Focus
focused: bool = false,
// Debounce (para callbacks externos)
last_callback_time: i64 = 0,
pending_row_callback: ?usize = null,
pub fn init(allocator: std.mem.Allocator) AdvancedTableState { ... }
pub fn deinit(self: *AdvancedTableState) void { ... }
// Row operations
pub fn setRows(self: *AdvancedTableState, rows: []const Row) void { ... }
pub fn getRowCount(self: *AdvancedTableState) usize { ... }
pub fn getRow(self: *AdvancedTableState, index: usize) ?*Row { ... }
pub fn insertRow(self: *AdvancedTableState, index: usize) void { ... }
pub fn deleteRow(self: *AdvancedTableState, index: usize) void { ... }
pub fn moveRowUp(self: *AdvancedTableState, index: usize) void { ... }
pub fn moveRowDown(self: *AdvancedTableState, index: usize) void { ... }
// State queries
pub fn getRowState(self: *AdvancedTableState, index: usize) RowState { ... }
pub fn markDirty(self: *AdvancedTableState, index: usize) void { ... }
pub fn markNew(self: *AdvancedTableState, index: usize) void { ... }
pub fn markDeleted(self: *AdvancedTableState, index: usize) void { ... }
pub fn clearRowState(self: *AdvancedTableState, index: usize) void { ... }
// Snapshots (para Auto-CRUD)
pub fn captureSnapshot(self: *AdvancedTableState, index: usize) void { ... }
pub fn getSnapshot(self: *AdvancedTableState, index: usize) ?Row { ... }
pub fn clearSnapshot(self: *AdvancedTableState, index: usize) void { ... }
// Selection
pub fn selectCell(self: *AdvancedTableState, row: usize, col: usize) void { ... }
pub fn clearSelection(self: *AdvancedTableState) void { ... }
// Editing
pub fn startEditing(self: *AdvancedTableState, initial_value: []const u8) void { ... }
pub fn stopEditing(self: *AdvancedTableState) void { ... }
pub fn getEditText(self: *AdvancedTableState) []const u8 { ... }
// Sorting
pub fn toggleSort(self: *AdvancedTableState, col: usize) void { ... }
pub fn restoreOriginalOrder(self: *AdvancedTableState) void { ... }
};
```
### 2.5 DataStore Interface
```zig
// types.zig
/// Interface para persistencia (BD, archivo, API, etc.)
pub const DataStore = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
load: *const fn(ptr: *anyopaque) []Row,
save: *const fn(ptr: *anyopaque, row: *Row) anyerror!void,
delete: *const fn(ptr: *anyopaque, row: *Row) anyerror!void,
lookup: *const fn(ptr: *anyopaque, table: []const u8, key_col: []const u8, key_val: CellValue) ?Row,
};
pub fn load(self: DataStore) []Row {
return self.vtable.load(self.ptr);
}
pub fn save(self: DataStore, row: *Row) !void {
return self.vtable.save(self.ptr, row);
}
pub fn delete(self: DataStore, row: *Row) !void {
return self.vtable.delete(self.ptr, row);
}
pub fn lookup(self: DataStore, table: []const u8, key_col: []const u8, key_val: CellValue) ?Row {
return self.vtable.lookup(self.ptr, table, key_col, key_val);
}
};
```
---
## 3. Funcionalidades Detalladas
### 3.1 Edición Excel-Style
**Comportamiento**:
1. **Activar edición**: F2, Enter, Spacebar, o tecla alfanumérica
2. **Tecla alfanumérica**: Reemplaza contenido con esa letra, cursor al final
3. **F2/Enter/Space**: Muestra valor actual, selecciona todo
4. **Durante edición**:
- Arrow Up/Down: Commit + navega + auto-edit
- Tab: Commit + navega siguiente editable + auto-edit
- Shift+Tab: Commit + navega anterior + auto-edit
- Enter: Commit + navega abajo
- Escape (1): Revert al valor original (sigue editando)
- Escape (2): Cancel, sale de edición
5. **Commit**: Valida, parsea, guarda en row, marca dirty
### 3.2 Navegación Teclado
| Tecla | Acción |
|-------|--------|
| ↑/↓/←/→ | Navegar entre celdas |
| Tab | Siguiente celda editable |
| Shift+Tab | Anterior celda editable |
| Enter | Iniciar edición o navegar abajo |
| F2 | Iniciar edición |
| Escape | Cancelar edición / Revert cambios |
| Ctrl+N | Insertar fila en posición actual |
| Ctrl+A | Agregar fila al final |
| Ctrl+B / Delete | Borrar fila actual |
| Ctrl+↑ | Mover fila arriba |
| Ctrl+↓ | Mover fila abajo |
| Home | Ir a primera columna |
| End | Ir a última columna |
| Ctrl+Home | Ir a primera fila |
| Ctrl+End | Ir a última fila |
| Page Up/Down | Scroll por página |
### 3.3 Auto-CRUD
**Flujo**:
1. Usuario entra en fila → `captureSnapshot(rowIndex)`
2. Usuario edita celdas → row modificado, `markDirty(rowIndex)`
3. Usuario sale de fila (navega a otra) → `detectCRUDAction(rowIndex)`
4. Según resultado:
- **ActionNone**: No hubo cambios reales → limpiar dirty flag
- **ActionCreate**: Snapshot vacío, current tiene datos → `DataStore.save()` (INSERT)
- **ActionUpdate**: Snapshot y current tienen datos diferentes → `DataStore.save()` (UPDATE)
- **ActionDelete**: Row marcado deleted o current vacío → `DataStore.delete()`
**Detección**:
```zig
fn detectCRUDAction(state: *AdvancedTableState, row_index: usize) CRUDAction {
// Check deleted flag first (Ctrl+B)
if (state.deleted_rows.get(row_index)) |_| {
return .delete;
}
const snapshot = state.getSnapshot(row_index) orelse return .none;
const current = state.getRow(row_index) orelse return .none;
const was_empty = isRowEmpty(snapshot);
const is_empty = isRowEmpty(current);
const has_changes = rowHasChanges(snapshot, current);
if (was_empty and !is_empty) return .create;
if (!was_empty and !is_empty and has_changes) return .update;
if (!was_empty and is_empty) return .delete;
return .none;
}
```
### 3.4 Sorting
**Ciclo**: Click en header → Ascending → Click → Descending → Click → Restore original
**Implementación**:
1. Primer sort en columna: Guardar orden original
2. Sort usa índices indirectos (no modifica rows directamente)
3. Después de sort: Sincronizar TODOS los state maps (dirty, new, deleted, snapshots, selection)
4. Restore: Volver al orden guardado
### 3.5 Visual States
| Estado | Color Background | Indicador |
|--------|------------------|-----------|
| Normal | Theme default | - |
| Modified | Amarillo suave | ● amarillo |
| New | Verde suave | ● verde |
| Deleted | Rojo suave | ● rojo |
| Error | Rojo intenso | ⚠ |
| Selected Cell | Azul intenso | - |
| Selected Row | Azul suave | - |
| Read-Only | Gris claro | 🔒 |
| Locked | Gris oscuro | 🔐 |
### 3.6 Lookup & Auto-fill
**Caso de uso**: Líneas de factura
- Usuario escribe código de producto
- Tabla busca en tabla `productos` por `codigo`
- Si encuentra: Auto-rellena `descripcion`, `precio`, `iva_porc`, `re_porc`
- Si no encuentra: Callback `OnLookupNotFound` (puede abrir diálogo crear producto)
**Configuración en ColumnDef**:
```zig
ColumnDef{
.name = "producto_codigo",
.title = "Código",
.enable_lookup = true,
.lookup_table = "productos",
.lookup_key_column = "codigo",
.auto_fill_columns = &[_]AutoFillMapping{
.{ .source_field = "descripcion", .target_column = "descripcion" },
.{ .source_field = "precio_venta", .target_column = "precio" },
.{ .source_field = "iva_porcentaje", .target_column = "iva_porc" },
.{ .source_field = "re_porcentaje", .target_column = "re_porc" },
},
}
```
---
## 4. API Pública
```zig
// advanced_table.zig
/// Dibujar AdvancedTable con schema y state
pub fn advancedTable(
ctx: *Context,
state: *AdvancedTableState,
schema: *const TableSchema,
) AdvancedTableResult;
/// Dibujar en rectángulo específico
pub fn advancedTableRect(
ctx: *Context,
bounds: Layout.Rect,
state: *AdvancedTableState,
schema: *const TableSchema,
colors: ?*const TableColors,
) AdvancedTableResult;
/// Resultado de interacción
pub const AdvancedTableResult = struct {
// Selección
selection_changed: bool = false,
selected_row: ?usize = null,
selected_col: ?usize = null,
// Edición
edit_started: bool = false,
edit_ended: bool = false,
cell_edited: bool = false,
// Sorting
sort_changed: bool = false,
sort_column: ?usize = null,
sort_ascending: bool = true,
// Row operations
row_inserted: bool = false,
row_deleted: bool = false,
row_moved: bool = false,
// Auto-CRUD (si habilitado)
crud_action: ?CRUDAction = null,
crud_error: ?[]const u8 = null,
// Lookup
lookup_triggered: bool = false,
lookup_found: bool = false,
};
```
---
## 5. Comparativa con Table actual
| Feature | Table actual | AdvancedTable |
|---------|-------------|---------------|
| Config | Callbacks `getCellData` | Schema-driven |
| Edición | Overlay básico | Excel-style completo |
| Auto-CRUD | Manual | Automático |
| Lookup | No | Sí |
| Calculated | No | Sí (futuro) |
| Row types | No | Sí (futuro) |
| Row locking | No | Sí |
| Sorting sync | Básico | Completo |
| State maps | Básico | Sparse optimizado |
| Callbacks | Legacy | Jerarquía Per-Column → Global → Default |
| Debounce | No | Sí (150ms default) |
---
## 6. Fases de Implementación
### Fase 1: Core (~600 LOC)
- [ ] types.zig - Tipos básicos
- [ ] schema.zig - TableSchema, ColumnDef
- [ ] state.zig - AdvancedTableState
- [ ] advanced_table.zig - Rendering básico
### Fase 2: Navegación (~300 LOC)
- [ ] navigation.zig - Keyboard navigation
- [ ] Integración con FocusSystem
### Fase 3: Edición (~400 LOC)
- [ ] editing.zig - Entry overlay, commit/cancel
- [ ] Parsers y formatters por tipo
### Fase 4: Sorting (~200 LOC)
- [ ] sorting.zig - Sort por columna
- [ ] Sync de state maps
### Fase 5: Row Operations (~250 LOC)
- [ ] row_ops.zig - Insert/Delete/Move
- [ ] State map management
### Fase 6: Auto-CRUD (~300 LOC)
- [ ] autocrud.zig - Detection & execution
- [ ] DataStore interface
- [ ] Snapshots
### Fase 7: Lookup (~200 LOC)
- [ ] lookup.zig - Lookup & auto-fill
### Fase 8: Callbacks (~150 LOC)
- [ ] callbacks.zig - Sistema jerárquico
- [ ] Debounce
**Total estimado**: ~2,400 LOC
---
## 7. Ejemplo de Uso
```zig
// Panel WHO List usando AdvancedTable
const who_schema = TableSchema{
.table_name = "who",
.columns = &[_]ColumnDef{
.{ .name = "id", .title = "ID", .width = 50, .editable = false },
.{ .name = "serie", .title = "Serie", .width = 80 },
.{ .name = "numero", .title = "Número", .width = 80, .column_type = .integer },
.{ .name = "fecha", .title = "Fecha", .width = 100, .column_type = .date },
.{ .name = "cliente_nif", .title = "NIF", .width = 100,
.enable_lookup = true,
.lookup_table = "clientes",
.lookup_key_column = "nif",
.auto_fill_columns = &[_]AutoFillMapping{
.{ .source_field = "nombre", .target_column = "cliente_nombre" },
.{ .source_field = "direccion", .target_column = "cliente_direccion" },
},
},
.{ .name = "cliente_nombre", .title = "Cliente", .width = 200 },
.{ .name = "base", .title = "Base", .width = 100, .column_type = .money, .align = .right },
.{ .name = "total", .title = "Total", .width = 100, .column_type = .money, .align = .right },
},
.auto_crud_enabled = true,
.show_row_state_indicators = true,
.data_store = &who_data_store,
};
var who_state = AdvancedTableState.init(allocator);
defer who_state.deinit();
// En el loop de UI:
const result = advancedTable(&ctx, &who_state, &who_schema);
if (result.selection_changed) {
// Actualizar panel de detalle
if (result.selected_row) |row_idx| {
loadDocumentDetail(who_state.getRow(row_idx));
}
}
if (result.crud_action) |action| {
switch (action) {
.create => showStatus("Documento creado", .success),
.update => showStatus("Documento guardado", .success),
.delete => showStatus("Documento borrado", .success),
.none => {},
}
}
```
---
## 8. Referencias
- **Go Implementation**: `/mnt/cello2/arno/re/recode/go/simifactu-fyne/internal/ui/components/advanced_table/`
- **zcatgui Table actual**: `src/widgets/table/`
- **DVUI Audit**: `docs/research/DVUI_AUDIT_2025-12-17.md`