feat(web): asiento con «Cuenta / socio», «Nombre» y «Cuenta» (solo asociadas para socios, repetida para cuentas)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 11:22:52 -05:00
1 parent c567bc6836
commit 77270d6909
6 files changed
+145 -24

No files matched your search

@@ -66,6 +66,14 @@ export interface DetailColumn {
lookups?: readonly DetailLookupOption[];
/** Texto que se ve en vez del código guardado, para una columna con varias listas (en una con `lookup`, va en la lista). */
codes?: CodeDisplay;
/** La lista de una celda `lookup` que depende de la fila (la «Cuenta» de un socio solo admite cuentas asociadas); sin ella, `lookup`. */
lookupFor?: (row: Record<string, unknown>) => VhLookup | undefined;
/** La celda no se edita en esta fila. */
readonlyFor?: (row: Record<string, unknown>) => boolean;
/** Texto calculado de la fila (solo se muestra; no es un campo que se guarde). */
computed?: (row: Record<string, unknown>) => string;
/** Campos de la fila que cambian además del propio al elegir o escribir un valor de la lista (la cuenta elegida primero completa el «Cuenta / socio»). */
pick?: (picked: ListRow, row: Record<string, unknown>) => Record<string, unknown>;
}
/** Una de las listas de una celda con varias: al elegir, `apply` devuelve los campos de la fila que cambian. */
+23 -14
View File
@@ -38,10 +38,10 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
<tr>
<td class="rn">{{ i + 1 }}</td>
@for (c of columns(); track c.key) {
<td [class.ro]="locked(c)">
<td [class.ro]="locked(c, row)">
@if (c.type === 'select') {
<select
[disabled]="locked(c)"
[disabled]="locked(c, row)"
[attr.data-r]="i"
[attr.aria-label]="c.title + ' de la fila ' + (i + 1)"
(change)="setSelect(i, c, $any($event.target).value)"
@@ -55,14 +55,14 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
<div class="pick">
<input
autocomplete="off"
[readOnly]="locked(c)"
[readOnly]="locked(c, row)"
[attr.data-r]="i"
[attr.aria-label]="c.title + ' de la fila ' + (i + 1)"
[value]="text(row, c)"
(change)="set(i, c, $any($event.target))"
(keydown.f4)="$event.preventDefault(); openLookup(i, c, $event)"
/>
@if (!locked(c)) {
@if (!locked(c, row)) {
<button type="button" tabindex="-1" [attr.aria-label]="'Abrir la lista de ' + c.title" title="Lista de valores (F4)" (click)="openLookup(i, c, $event)">…</button>
}
</div>
@@ -71,7 +71,7 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
[type]="c.type === 'date' ? 'date' : 'text'"
autocomplete="off"
[class.num]="c.type === 'number'"
[readOnly]="locked(c)"
[readOnly]="locked(c, row)"
[attr.data-r]="i"
[attr.aria-label]="c.title + ' de la fila ' + (i + 1)"
[value]="text(row, c)"
@@ -168,13 +168,20 @@ export class RowsTable {
/** El menú de listas de una celda con varias (cuentas, clientes, proveedores), con su posición en pantalla. */
protected readonly menu = signal<{ row: number; col: DetailColumn; x: number; y: number } | null>(null);
/** La lista de una celda en una fila: la que depende de la fila o, si no, la de la columna. */
private lookupOf(row: Row | undefined, c: DetailColumn): NonNullable<DetailColumn['lookup']> | undefined {
return (row && c.lookupFor?.(row)) || c.lookup;
}
protected openLookup(row: number, col: DetailColumn, event?: Event): void {
if (this.locked(col)) return;
const line = this.rows()[row];
if (this.locked(col, line)) return;
if (col.lookups?.length) {
const box = (event?.target as HTMLElement | undefined)?.getBoundingClientRect?.();
this.menu.set({ row, col, x: box?.left ?? 0, y: box?.bottom ?? 0 });
} else if (col.lookup) {
this.target.set({ row, col, lookup: col.lookup });
} else {
const lookup = this.lookupOf(line, col);
if (lookup) this.target.set({ row, col, lookup });
}
}
@@ -195,18 +202,19 @@ export class RowsTable {
return;
}
const key = t.lookup.valueKey ?? t.lookup.columns[0]?.key ?? '';
this.rows.update((rs) => rs.map((r, i) => (i === t.row ? { ...r, [t.col.key]: picked[key] ?? '' } : r)));
this.rows.update((rs) => rs.map((r, i) => (i === t.row ? { ...r, ...t.col.pick?.(picked, r), [t.col.key]: picked[key] ?? '' } : r)));
}
protected canRemove(): boolean {
return !this.readonly() && this.removable();
}
protected locked(c: DetailColumn): boolean {
return this.readonly() || !!c.readonly;
protected locked(c: DetailColumn, row?: Row): boolean {
return this.readonly() || !!c.readonly || !!c.computed || (!!row && !!c.readonlyFor?.(row));
}
protected text(row: Row, c: DetailColumn): string {
if (c.computed) return c.computed(row);
const v = row[c.key];
if (c.type === 'date') return dateOnly(v);
if (c.type === 'number') return v === null || v === undefined || v === '' ? '' : formatNumber(v, c.decimals);
@@ -236,7 +244,7 @@ export class RowsTable {
input.value = parsed === null ? '' : formatNumber(parsed, c.decimals);
}
this.rows.update((rs) => rs.map((r, i) => (i === index ? { ...r, [c.key]: value } : r)));
if (c.type === 'lookup' && (c.lookup?.codes || c.lookups?.length) && raw.trim() !== '') void this.resolveTyped(index, c, raw.trim());
if (c.type === 'lookup' && (c.lookup?.codes || c.lookups?.length || c.lookupFor) && raw.trim() !== '') void this.resolveTyped(index, c, raw.trim());
}
/**
@@ -273,13 +281,14 @@ export class RowsTable {
}
return;
}
const lookup = c.lookup!;
const lookup = this.lookupOf(this.rows()[index], c);
if (!lookup) return;
const found = await this.lookupApi.find(lookup, code);
if (!found) return;
lookup.codes?.remember(found);
const key = lookup.valueKey ?? lookup.columns[0]?.key ?? '';
const value = found[key] ?? code;
this.rows.update((rs) => rs.map((r, i) => (i === index && r[c.key] === code ? { ...r, [c.key]: value } : r)));
this.rows.update((rs) => rs.map((r, i) => (i === index && r[c.key] === code ? { ...r, ...c.pick?.(found, r), [c.key]: value } : r)));
} catch {
/* sin conexión: queda lo escrito */
}