feat(web): el selector de artículo de una línea admite varios (el primero completa la línea, los demás se insertan debajo)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1528f7cb96
commit
45c0ca536e
5 files changed
+37
-1
No files matched your search
@@ -49,6 +49,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [
|
|||||||
{ key: 'ItemName', title: 'Descripción' },
|
{ key: 'ItemName', title: 'Descripción' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
multiple: true,
|
||||||
apply: (item, line) => ({ ...line, ItemCode: item['ItemCode'], ItemDescription: item['ItemName'] ?? line['ItemDescription'] }),
|
apply: (item, line) => ({ ...line, ItemCode: item['ItemCode'], ItemDescription: item['ItemName'] ?? line['ItemDescription'] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export type GridLine = Record<string, unknown>;
|
|||||||
export interface GridLookup {
|
export interface GridLookup {
|
||||||
config: VhLookup;
|
config: VhLookup;
|
||||||
apply: (picked: ListRow, line: GridLine) => GridLine;
|
apply: (picked: ListRow, line: GridLine) => GridLine;
|
||||||
|
/** Permite marcar varias filas: la primera completa la línea actual y cada una de las demás crea una línea nueva. */
|
||||||
|
multiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridColumn {
|
export interface GridColumn {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
[baseFilter]="cfg.baseFilter"
|
[baseFilter]="cfg.baseFilter"
|
||||||
[orderby]="cfg.orderby"
|
[orderby]="cfg.orderby"
|
||||||
[rowKey]="cfg.valueKey"
|
[rowKey]="cfg.valueKey"
|
||||||
|
[selection]="lookupMultiple() ? 'multiple' : 'single'"
|
||||||
(selected)="lookupPicked($event)"
|
(selected)="lookupPicked($event)"
|
||||||
(closed)="lookupClosed()"
|
(closed)="lookupClosed()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -218,11 +218,34 @@ describe('LinesGrid', () => {
|
|||||||
await tick();
|
await tick();
|
||||||
(q('.dlg tbody tr') as HTMLElement).click();
|
(q('.dlg tbody tr') as HTMLElement).click();
|
||||||
await tick();
|
await tick();
|
||||||
|
(q('.dlg footer .btn.main') as HTMLElement).click();
|
||||||
|
await tick();
|
||||||
expect(lines()[0]).toMatchObject({ ItemCode: 'I00002', ItemDescription: 'DVD+R Disc 50-Pack', Quantity: 2, Price: 50, LineTotal: 100 });
|
expect(lines()[0]).toMatchObject({ ItemCode: 'I00002', ItemDescription: 'DVD+R Disc 50-Pack', Quantity: 2, Price: 50, LineTotal: 100 });
|
||||||
expect(el.querySelector('.dlg')).toBeNull();
|
expect(el.querySelector('.dlg')).toBeNull();
|
||||||
expect(document.activeElement).toBe(cell(0, 0));
|
expect(document.activeElement).toBe(cell(0, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('varios artículos: el primero completa la línea y los demás se insertan debajo', async () => {
|
||||||
|
fixture.componentInstance.lines.set([line('A', 2, 50), line('Z', 1, 10)]);
|
||||||
|
await tick();
|
||||||
|
(qa('.pick button')[0] as HTMLButtonElement).click();
|
||||||
|
await tick();
|
||||||
|
ctrl.expectOne((r) => r.url === '/api/sl/Items').flush({
|
||||||
|
value: [
|
||||||
|
{ ItemCode: 'I1', ItemName: 'Uno' },
|
||||||
|
{ ItemCode: 'I2', ItemName: 'Dos' },
|
||||||
|
],
|
||||||
|
'odata.count': '2',
|
||||||
|
});
|
||||||
|
await tick();
|
||||||
|
qa('.dlg tbody tr').forEach((r) => (r as HTMLElement).click());
|
||||||
|
await tick();
|
||||||
|
(q('.dlg footer .btn.main') as HTMLElement).click();
|
||||||
|
await tick();
|
||||||
|
expect(lines().map((l) => l['ItemCode'])).toEqual(['I1', 'I2', 'Z']);
|
||||||
|
expect(lines()[0]).toMatchObject({ Quantity: 2, Price: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
it('F4 abre la lista de la celda; una celda sin lista no', async () => {
|
it('F4 abre la lista de la celda; una celda sin lista no', async () => {
|
||||||
await key(0, 3, 'F4'); // almacén
|
await key(0, 3, 'F4'); // almacén
|
||||||
ctrl.expectOne((r) => r.url === '/api/sl/Warehouses').flush({ value: [], 'odata.count': '0' });
|
ctrl.expectOne((r) => r.url === '/api/sl/Warehouses').flush({ value: [], 'odata.count': '0' });
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export class LinesGrid {
|
|||||||
protected readonly lookupTarget = signal<LookupTarget | null>(null);
|
protected readonly lookupTarget = signal<LookupTarget | null>(null);
|
||||||
protected readonly multiOpen = signal(false);
|
protected readonly multiOpen = signal(false);
|
||||||
protected readonly lookup = computed(() => this.lookupTarget()?.column.lookup?.config);
|
protected readonly lookup = computed(() => this.lookupTarget()?.column.lookup?.config);
|
||||||
|
protected readonly lookupMultiple = computed(() => !!this.lookupTarget()?.column.lookup?.multiple);
|
||||||
|
|
||||||
protected cellText(line: GridLine, column: GridColumn): string {
|
protected cellText(line: GridLine, column: GridColumn): string {
|
||||||
if (column.format) return column.format(line);
|
if (column.format) return column.format(line);
|
||||||
@@ -216,7 +217,15 @@ export class LinesGrid {
|
|||||||
const picked = rows[0];
|
const picked = rows[0];
|
||||||
const line = target ? this.lines()[target.row] : undefined;
|
const line = target ? this.lines()[target.row] : undefined;
|
||||||
if (!target || !picked || !line || !target.column.lookup) return;
|
if (!target || !picked || !line || !target.column.lookup) return;
|
||||||
this.replace(target.row, target.column.lookup.apply(picked, line), target.column.key);
|
const lookup = target.column.lookup;
|
||||||
|
this.replace(target.row, lookup.apply(picked, line), target.column.key);
|
||||||
|
const rest = rows.slice(1).map((r) => {
|
||||||
|
const added = lookup.apply(r, this.newLine()());
|
||||||
|
return this.recalc()?.(added, target.column.key) ?? added;
|
||||||
|
});
|
||||||
|
if (rest.length) {
|
||||||
|
this.lines.update((ls) => [...ls.slice(0, target.row + 1), ...rest, ...ls.slice(target.row + 1)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected lookupClosed(): void {
|
protected lookupClosed(): void {
|
||||||
|
|||||||
Reference in new issue
Block a user