# 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`