feat(web): lotes y series en las líneas de entradas, salidas y transferencias de stock
Una ventana por línea con tabla editable; validación de suma de lotes y una serie por unidad. Probado contra SBODemoCL: entrada de inventario 156 con SU01 y su lote. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5592fac76
commit
abc1cc4ac6
16 files changed
+752
-10
No files matched your search
@@ -16,7 +16,8 @@ import { SlLookupApi } from '../../core/sl/sl-lookup';
|
||||
import type { ListRow } from '../list/list.types';
|
||||
import { ColResize } from '../table/col-resize';
|
||||
import { ValueHelp } from '../value-help/value-help';
|
||||
import type { GridColumn, GridLine, GridMultiAdd } from './grid.types';
|
||||
import type { GridColumn, GridDetail, GridLine, GridMultiAdd } from './grid.types';
|
||||
import { LineDetailDialog } from './line-detail-dialog';
|
||||
|
||||
interface LookupTarget {
|
||||
row: number;
|
||||
@@ -31,7 +32,7 @@ interface LookupTarget {
|
||||
@Component({
|
||||
selector: 'sbo-lines-grid',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ValueHelp, ColResize],
|
||||
imports: [ValueHelp, ColResize, LineDetailDialog],
|
||||
templateUrl: './lines-grid.html',
|
||||
styleUrl: './lines-grid.css',
|
||||
})
|
||||
@@ -45,6 +46,10 @@ export class LinesGrid {
|
||||
/** Recalcula la línea tras un cambio (ej. total = cantidad × precio). */
|
||||
readonly recalc = input<((line: GridLine, changedKey: string) => GridLine) | undefined>(undefined);
|
||||
readonly multiAdd = input<GridMultiAdd>();
|
||||
/** Detalles de línea (lotes, series): una columna de botones que abre su ventana de edición. */
|
||||
readonly details = input<readonly GridDetail[]>([]);
|
||||
/** Dado el detalle y las filas en edición, el aviso bajo la tabla de la ventana (ej. «suman 8 de 10»). */
|
||||
readonly detailHint = input<((detail: GridDetail, line: GridLine, rows: readonly Record<string, unknown>[]) => { text: string; bad: boolean }) | undefined>(undefined);
|
||||
readonly addLabel = input('Añadir línea');
|
||||
/** Avisos de la grilla (ej. un código escrito que no existe), para la barra de estado del formulario. */
|
||||
readonly notice = output<string>();
|
||||
@@ -53,8 +58,22 @@ export class LinesGrid {
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
protected readonly lookupTarget = signal<LookupTarget | null>(null);
|
||||
protected readonly detailTarget = signal<{ row: number; detail: GridDetail } | null>(null);
|
||||
protected readonly detailHintFn = computed(() => {
|
||||
const t = this.detailTarget();
|
||||
const hint = this.detailHint();
|
||||
const line = t ? this.lines()[t.row] : undefined;
|
||||
return t && hint && line ? (rows: readonly Record<string, unknown>[]) => hint(t.detail, line, rows) : undefined;
|
||||
});
|
||||
protected readonly multiOpen = signal(false);
|
||||
protected readonly lookup = computed(() => this.lookupTarget()?.column.lookup?.config);
|
||||
protected readonly hasDetails = computed(() => this.details().length > 0);
|
||||
/** Filas con que se abre la ventana (estable mientras no cambie la línea: la ventana edita una copia). */
|
||||
protected readonly detailInitial = computed<readonly Record<string, unknown>[]>(() => {
|
||||
const t = this.detailTarget();
|
||||
const line = t ? this.lines()[t.row] : undefined;
|
||||
return (line?.[t?.detail.key ?? ''] as Record<string, unknown>[] | undefined) ?? [];
|
||||
});
|
||||
|
||||
/**
|
||||
* Ancho mínimo de la tabla: la suma de los anchos de sus columnas (las que no declaran uno, como la
|
||||
@@ -66,7 +85,7 @@ export class LinesGrid {
|
||||
const w = parseFloat(c.width ?? '');
|
||||
return Number.isFinite(w) ? w : 220;
|
||||
};
|
||||
return this.columns().reduce((sum, c) => sum + px(c), 34 + (this.readonly() ? 0 : 30));
|
||||
return this.columns().reduce((sum, c) => sum + px(c), 34 + (this.readonly() ? 0 : 30) + (this.hasDetails() ? 130 : 0));
|
||||
});
|
||||
protected readonly lookupMultiple = computed(() => !!this.lookupTarget()?.column.lookup?.multiple);
|
||||
|
||||
@@ -231,6 +250,24 @@ export class LinesGrid {
|
||||
if (!focus()) afterNextRender(() => void focus(), { injector: this.injector });
|
||||
}
|
||||
|
||||
// --- detalles de línea (lotes, series) ----------------------------------------------------
|
||||
|
||||
/** Los detalles que aplican a una línea. */
|
||||
protected detailsOf(line: GridLine): readonly GridDetail[] {
|
||||
return this.details().filter((d) => d.applies(line));
|
||||
}
|
||||
|
||||
protected openDetail(row: number, detail: GridDetail): void {
|
||||
this.detailTarget.set({ row, detail });
|
||||
}
|
||||
|
||||
protected detailSaved(rows: Record<string, unknown>[]): void {
|
||||
const t = this.detailTarget();
|
||||
if (!t) return;
|
||||
this.lines.update((ls) => ls.map((l, i) => (i === t.row ? { ...l, [t.detail.key]: rows } : l)));
|
||||
this.detailTarget.set(null);
|
||||
}
|
||||
|
||||
// --- listas de valores -----------------------------------------------------------------
|
||||
|
||||
protected openLookup(row: number, column: GridColumn): void {
|
||||
|
||||
Reference in new issue
Block a user