import { ChangeDetectionStrategy, Component, booleanAttribute, computed, Injector, effect, 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'; /** Nombres ya consultados de los códigos de las listas con `nameKey`. */ const NAMES = new Map(); /** * 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: ` @if (lookup().nameKey) { } `, styles: ` :host { display: block; } `, }) export class LookupField extends FieldBase { readonly value = model(''); readonly lookup = input.required(); /** 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(); readonly linkClick = output(); 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('box'); /** Nombre de lo elegido, junto al código (`lookup.nameKey`). */ protected readonly name = signal(''); private nameFor = ''; /** El usuario está escribiendo: se muestra lo que escribe y no el texto con que la lista presenta el código guardado. */ private readonly editing = signal(false); /** * Lo que se ve en el campo: el código guardado o, en una lista que tiene texto propio para él (la cuenta con su * código con formato en vez del interno `_SYS…`), ese texto. */ protected readonly shown = computed(() => { const v = this.value(); const codes = this.lookup().codes; return !this.editing() && codes && v !== '' ? codes.display(v) : v; }); constructor() { super(); effect(() => { const v = this.value(); const codes = this.lookup().codes; // un valor que llega de fuera (documento cargado, valor por defecto) ya está resuelto y se pide su texto; lo que se escribe, no if (!this.editing()) { if (codes && v !== '') codes.ensure([v]); this.resolved = v.trim(); void this.loadName(v.trim()); } }); } /** El nombre del código guardado; se pregunta una vez por código (el resultado queda en `NAMES`). */ private async loadName(code: string): Promise { const lookup = this.lookup(); const nameKey = lookup.nameKey; this.nameFor = code; if (!nameKey || !code) return void this.name.set(''); const cacheKey = `${lookup.resource}|${lookup.baseFilter ?? ''}|${code}`; const cached = NAMES.get(cacheKey); if (cached !== undefined) return void this.name.set(cached); try { const row = await this.injector.get(SlLookupApi).find(lookup, code); const text = row ? String(row[nameKey] ?? '') : ''; NAMES.set(cacheKey, text); if (this.nameFor === code) this.name.set(text); } catch { if (this.nameFor === code) this.name.set(''); } } protected typing(text: string): void { this.editing.set(true); this.value.set(text); } 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.lookup().codes?.remember(row); const nameKey = this.lookup().nameKey; if (nameKey) { this.nameFor = String(row[this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? ''] ?? ''); this.name.set(String(row[nameKey] ?? '')); } this.editing.set(false); this.value.set(String(row[key] ?? '')); this.resolved = this.value(); this.picked.emit(row); } protected async resolveTyped(): Promise { const code = this.value().trim(); if (!this.editing()) return; // no se escribió nada: lo guardado ya está resuelto if (!this.resolveOnLeave() || this.readOnly() || this.disabled() || this.listOpen()) { this.editing.set(false); return; } if (!code) { this.missing.set(null); this.resolved = ''; this.editing.set(false); return; } if (code === this.resolved) { this.editing.set(false); 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.' }); } finally { if (this.value().trim() === code) this.editing.set(false); } } }