Files
basic-erp/apps/web/src/app/shared/grid/line-detail-dialog.ts
T
GeorgeandClaude Sonnet 5.5 4e9d6a757d feat(web): tablas en los maestros (direcciones, personas de contacto, precios por lista, stock por almacén)
sbo-rows-table reutilizable (también en la ventana de lotes). Direcciones y contactos se envían
completos con reemplazo; los precios solo por filas cambiadas. Probado contra SBODemoCL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 12:31:27 -05:00

82 lines
3.8 KiB
TypeScript

import { ChangeDetectionStrategy, Component, computed, input, linkedSignal, output } from '@angular/core';
import type { DetailColumn } from './grid.types';
import { RowsTable } from './rows-table';
type Row = Record<string, unknown>;
/**
* Ventana con las filas de un detalle de línea (lotes, números de serie): tabla editable con «Añadir fila»
* y «Quitar». Solo emite `saved` al aceptar; cancelar descarta los cambios.
*/
@Component({
selector: 'sbo-line-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RowsTable],
template: `
<div class="overlay">
<div class="dlg" role="dialog" aria-modal="true" [attr.aria-label]="title()" (keydown.escape)="closed.emit()">
<header>
<h2>{{ title() }}</h2>
@if (subtitle()) {
<span class="sub">{{ subtitle() }}</span>
}
</header>
<div class="body">
<sbo-rows-table [columns]="columns()" [(rows)]="rows" [readonly]="readonly()" [emptyText]="readonly() ? 'Sin filas.' : 'Sin filas. Pulse «Añadir fila».'" />
</div>
@if (hint()) {
<p class="hint" [class.bad]="bad()">{{ hint() }}</p>
}
<footer>
@if (!readonly()) {
<button class="btn" type="button" (click)="add()">+ Añadir fila</button>
}
<span class="sp"></span>
<button class="btn" type="button" (click)="closed.emit()">{{ readonly() ? 'Cerrar' : 'Cancelar' }}</button>
@if (!readonly()) {
<button class="btn main" type="button" (click)="saved.emit(clean())">Aceptar</button>
}
</footer>
</div>
</div>
`,
styles: `
.overlay { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 16px; background: var(--overlay); }
.dlg { display: flex; flex-direction: column; width: min(720px, 100%); max-height: 80vh; background: var(--win); border: 1px solid var(--line); border-radius: var(--radius-panel); overflow: hidden; box-shadow: var(--shadow-pop); }
header { display: flex; align-items: baseline; gap: 10px; padding: 8px 12px; background: var(--panel); border-bottom: 1px solid var(--line); }
h2 { margin: 0; font-size: 14px; font-weight: 600; }
.sub { font-size: 12px; color: var(--quiet); }
.body { overflow: auto; padding: 8px 12px; }
.hint { margin: 0; padding: 6px 12px; font-size: 12px; color: var(--quiet); border-top: 1px solid var(--line-soft); }
.hint.bad { color: var(--err); }
footer { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-top: 1px solid var(--line); background: var(--panel); }
.sp { flex: 1; }
`,
})
export class LineDetailDialog {
readonly title = input.required<string>();
readonly subtitle = input('');
readonly columns = input.required<readonly DetailColumn[]>();
readonly initial = input<readonly Row[]>([]);
readonly readonly = input(false);
/** Texto bajo la tabla, calculado por quien abre la ventana con las filas en edición. */
readonly hintFor = input<((rows: readonly Row[]) => { text: string; bad: boolean }) | undefined>(undefined);
readonly saved = output<Row[]>();
readonly closed = output<void>();
protected readonly rows = linkedSignal<Row[]>(() => this.initial().map((r) => ({ ...r })));
protected readonly hintState = computed(() => this.hintFor()?.(this.rows()) ?? { text: '', bad: false });
protected readonly hint = computed(() => this.hintState().text);
protected readonly bad = computed(() => this.hintState().bad);
protected add(): void {
this.rows.update((rs) => [...rs, {}]);
}
/** Las filas sin ningún dato no se guardan. */
protected clean(): Row[] {
return this.rows().filter((r) => this.columns().some((c) => r[c.key] !== null && r[c.key] !== undefined && r[c.key] !== ''));
}
}