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

18 KiB

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

// 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

// 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

// 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

// 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:

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:

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

// 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
Calculated No Sí (futuro)
Row types No Sí (futuro)
Row locking No
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

// 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