Files
basic-erp/apps/web/src/app/shared/form/lookup-field.ts
T

136 lines
4.2 KiB
TypeScript
Raw Normal View History

import {
ChangeDetectionStrategy,
Component,
booleanAttribute,
computed,
Injector,
inject,
input,
model,
output,
signal,
viewChild,
} from '@angular/core';
import { SlLookupApi } from '../../core/sl/sl-lookup';
import { ValueHelp } from '../value-help/value-help';
import type { VhLookup, VhRow } from '../value-help/value-help.types';
import { TextInput } from './controls';
import { FieldBase } from './field-base';
import { SboField } from './sbo-field';
/**
* Campo con botón "…" que abre una lista de valores (flecha dorada de SAP).
* Escribir el código a mano también es válido; elegir de la lista lo rellena y emite `picked`.
*/
@Component({
selector: 'sbo-lookup-field',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SboField, TextInput, ValueHelp],
template: `
<sbo-field
[label]="label()"
[required]="required()"
[error]="error() || missingText()"
action
[linkable]="linkable()"
(labelClick)="linkClick.emit()"
>
<sbo-input
#box
[(value)]="value"
[readonly]="readOnly()"
[disabled]="disabled()"
[required]="required()"
(keydown.f4)="$event.preventDefault(); openList()"
(focusout)="resolveTyped()"
/>
<button
type="button"
class="sbo-btn"
title="Lista de valores (F4)"
[attr.aria-label]="'Abrir la lista de ' + label()"
[disabled]="readOnly() || disabled()"
(click)="openList()"
>
…
</button>
</sbo-field>
<sbo-value-help
[open]="listOpen()"
[title]="lookup().title"
[resource]="lookup().resource"
[columns]="lookup().columns"
[baseFilter]="lookup().baseFilter"
[orderby]="lookup().orderby"
(selected)="onPicked($event)"
(closed)="closeList()"
/>
`,
styles: ':host { display: block; }',
})
export class LookupField extends FieldBase {
readonly value = model('');
readonly lookup = input.required<VhLookup>();
/** La etiqueta abre el objeto enlazado (evento `linkClick`). */
readonly linkable = input(false, { transform: booleanAttribute });
/** Al salir del campo con un código escrito a mano, lo busca en la lista (como si se hubiera elegido). */
readonly resolveOnLeave = input(false, { transform: booleanAttribute });
readonly picked = output<VhRow>();
readonly linkClick = output<void>();
protected readonly listOpen = signal(false);
/** Código que no existe; el aviso desaparece en cuanto el texto cambia. */
private readonly missing = signal<{ code: string; text: string } | null>(null);
protected readonly missingText = computed(() => {
const m = this.missing();
return m && m.code === this.value().trim() ? m.text : '';
});
private readonly injector = inject(Injector);
/** Último código ya resuelto, para no repetir la consulta al salir y volver a entrar sin cambios. */
private resolved = '';
private readonly box = viewChild.required<TextInput>('box');
protected openList(): void {
if (!this.readOnly() && !this.disabled()) this.listOpen.set(true);
}
protected closeList(): void {
this.listOpen.set(false);
this.box().focus();
}
protected onPicked(rows: VhRow[]): void {
const row = rows[0];
if (!row) return;
this.apply(row);
}
private apply(row: VhRow): void {
const key = this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? '';
this.missing.set(null);
this.value.set(String(row[key] ?? ''));
this.resolved = this.value();
this.picked.emit(row);
}
protected async resolveTyped(): Promise<void> {
const code = this.value().trim();
if (!this.resolveOnLeave() || this.readOnly() || this.disabled() || this.listOpen()) return;
if (!code) {
this.missing.set(null);
this.resolved = '';
return;
}
if (code === this.resolved) return;
try {
const row = await this.injector.get(SlLookupApi).find(this.lookup(), code);
if (this.value().trim() !== code) return; // el usuario siguió escribiendo
if (row) this.apply(row);
else this.missing.set({ code, text: `No existe «${code}» en ${this.lookup().title.toLowerCase()}.` });
} catch {
this.missing.set({ code, text: 'No se pudo comprobar el código.' });
}
}
}