feat(web): Indicador, prefijo y número del folio juntos en una fila (documentos de facturación y asiento) y las tres fechas de la cabecera en orden

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 14:35:54 -05:00
1 parent 1bb8efa77e
commit a74c19a139
11 files changed
+170 -19

No files matched your search

+1 -1
View File
@@ -52,7 +52,7 @@ Es un ERP, no una página de publicidad: **ligera y rápida**. Sin librerías de
## Flujo de las pantallas de documento
Cada menú abre primero una **lista de los últimos documentos** (barra de filtros + búsqueda por columna); desde ahí se crea uno nuevo (`/…/nuevo`, formulario vacío) o se abre uno existente (`/…/:docEntry`, ver/editar; los cerrados/cancelados en solo lectura). Las pantallas se definen por módulo en `apps/web/src/app/features/documents/` (17 documentos): `ventas/ventas.screens.ts`, `compras/compras.screens.ts` (+ `compras.layouts.ts`) e `inventario/inventario.screens.ts` (+ `inventario.layouts.ts`); los maestros (socios de negocio, artículos) irán en su propia carpeta. `screen-kit.ts` tiene el tipo `DocumentScreen` y las columnas y filtros comunes; `document-screens.ts` las reúne en `DOCUMENT_SCREENS`. Las columnas de línea estándar están en `features/documents/line-columns.ts`. **Tipo Artículo / Servicio** (`DocType`): los documentos de ventas y compras con socio llevan en la cabecera la lista «Tipo» (primera de la columna izquierda; no se cambia en uno guardado). Con «Servicio» las líneas pasan a `layout.service` (`serviceLineColumns` en `line-columns.ts`): Cuenta contable (obligatoria), Descripción, Impuesto y Total (se escribe, sin recalcular), dimensiones, proyecto y referencias al origen; sin artículo, cantidad, almacén, precio, lotes ni «Elegir artículos». Se guardan `AccountCode`, `ItemDescription`, `TaxCode`, `LineTotal` (`LinesSpec.service`); cambiar el tipo con líneas escritas pide permiso y las descarta; «Copiar de» trae el tipo y el importe abierto (`OpenAmount`) del origen y no mezcla tipos. Las extensiones de cliente de columnas de línea aplican a las líneas de artículos; las de servicio aún no son extensibles.
Cada menú abre primero una **lista de los últimos documentos** (barra de filtros + búsqueda por columna); desde ahí se crea uno nuevo (`/…/nuevo`, formulario vacío) o se abre uno existente (`/…/:docEntry`, ver/editar; los cerrados/cancelados en solo lectura). Las pantallas se definen por módulo en `apps/web/src/app/features/documents/` (17 documentos): `ventas/ventas.screens.ts`, `compras/compras.screens.ts` (+ `compras.layouts.ts`) e `inventario/inventario.screens.ts` (+ `inventario.layouts.ts`); los maestros (socios de negocio, artículos) irán en su propia carpeta. `screen-kit.ts` tiene el tipo `DocumentScreen` y las columnas y filtros comunes; `document-screens.ts` las reúne en `DOCUMENT_SCREENS`. Las columnas de línea estándar están en `features/documents/line-columns.ts`. **Tipo Artículo / Servicio** (`DocType`): los documentos de ventas y compras con socio llevan en la cabecera la lista «Tipo» (primera de la columna izquierda; no se cambia en uno guardado). Con «Servicio» las líneas pasan a `layout.service` (`serviceLineColumns` en `line-columns.ts`): Cuenta contable (obligatoria), Descripción, Impuesto y Total (se escribe, sin recalcular), dimensiones, proyecto y referencias al origen; sin artículo, cantidad, almacén, precio, lotes ni «Elegir artículos». Se guardan `AccountCode`, `ItemDescription`, `TaxCode`, `LineTotal` (`LinesSpec.service`); cambiar el tipo con líneas escritas pide permiso y las descarta; «Copiar de» trae el tipo y el importe abierto (`OpenAmount`) del origen y no mezcla tipos. Las extensiones de cliente de columnas de línea aplican a las líneas de artículos; las de servicio aún no son extensibles. **Indicador y folio juntos y en ese orden** (`IDENTITY_GROUP`, un `HeaderField` tipo `group` = varias celdas en una fila: Indicador con su lista, prefijo y número del folio) en facturas, boletas, reservas y notas de crédito de ventas y compras (`BILLING_DOCUMENT`) y en el asiento. Una empresa que los sustituye por campos de usuario arma su grupo con `fieldGroup(label, cells)` en `headerRight` de su extensión y oculta el del base con `hideHeaderFields: ['Indicator']` (oculta el grupo completo). Las **tres fechas** de la cabecera de todo documento de ventas y compras van juntas y en este orden, una debajo de la otra: contabilización, vencimiento y del documento (`TaxDate`), las tres con la fecha de hoy al crear.
## Cómo verificar cambios de interfaz
@@ -252,10 +252,13 @@ describe('el proyecto de la cabecera se elige de su ventana modal', () => {
});
describe('el indicador de la cabecera se elige de su ventana modal', () => {
it('es un campo con lista de valores sobre FactoringIndicators, con código y nombre', () => {
const f = DOCUMENT_SCREENS.find((s) => s.path === '/ventas/facturas')!.layout.header.left.find((x) => x.type === 'lookup' && x.key === 'Indicator');
expect(f && f.type === 'lookup' && f.lookup.resource).toBe('FactoringIndicators');
expect(f && f.type === 'lookup' && f.lookup.columns.map((c) => c.key)).toEqual(['IndicatorCode', 'IndicatorName']);
it('es una celda con lista de valores sobre FactoringIndicators, con código y nombre, junto al folio', () => {
const group = DOCUMENT_SCREENS.find((s) => s.path === '/ventas/facturas')!.layout.header.right.find((x) => x.type === 'group');
const cells = group && group.type === 'group' ? group.cells : [];
expect(cells.map((c) => c.key)).toEqual(['Indicator', 'FolioPrefixString', 'FolioNumber']);
const first = cells[0];
expect(first.kind === 'lookup' && first.lookup.resource).toBe('FactoringIndicators');
expect(first.kind === 'lookup' && first.lookup.columns.map((c) => c.key)).toEqual(['IndicatorCode', 'IndicatorName']);
});
it('abre su ventana de selección y escribe el código elegido', async () => {
@@ -278,8 +281,8 @@ describe('el indicador de la cabecera se elige de su ventana modal', () => {
};
await tick();
const el = fixture.nativeElement as HTMLElement;
const row = [...el.querySelectorAll('sbo-lookup-field')].find((f) => f.querySelector('label')?.textContent?.trim() === 'Indicador')!;
expect(row, 'campo Indicador con lista').toBeDefined();
const row = [...el.querySelectorAll('sbo-field')].find((f) => f.querySelector('label')?.textContent?.trim() === 'Indicador / Folio')!;
expect(row, 'fila Indicador / Folio').toBeDefined();
(row.querySelector('button.sbo-btn') as HTMLElement).click();
await tick();
ctrl.expectOne((r) => r.url === '/api/sl/FactoringIndicators').flush({ value: [{ IndicatorCode: '33', IndicatorName: 'Factura' }, { IndicatorCode: '39', IndicatorName: 'Boleta' }], 'odata.count': '2' });
@@ -75,7 +75,7 @@ describe('folio y RUT en los formularios', () => {
it('una factura muestra folio, número de folio, referencia y RUT', async () => {
await open('PurchaseInvoices', invoice());
expect(labels()).toEqual(expect.arrayContaining(['Folio', 'Nº referencia', 'RUT']));
expect(labels()).toEqual(expect.arrayContaining(['Indicador / Folio', 'Nº referencia', 'RUT']));
const inputs = [...el().querySelectorAll<HTMLInputElement>('input')].map((i) => i.value);
expect(inputs).toEqual(expect.arrayContaining(['F001', '198837', 'REF-1', '76543210-K']));
});
@@ -72,6 +72,23 @@
}
</sbo-window>
@if (groupCell(); as cell) {
@if (cell.kind === 'lookup') {
<sbo-value-help
[open]="true"
[title]="cell.lookup.title"
[resource]="cell.lookup.resource"
[columns]="cell.lookup.columns"
[baseFilter]="cell.lookup.baseFilter"
[orderby]="cell.lookup.orderby"
[rowKey]="cell.lookup.valueKey"
[extraFields]="cell.lookup.extraFields ?? []"
(selected)="groupPicked($event)"
(closed)="groupCell.set(null)"
/>
}
}
@if (copyListScreen(); as src) {
<sbo-value-help
[open]="true"
@@ -184,6 +201,23 @@
/>
</sbo-field>
}
@case ('group') {
<sbo-field [label]="f.label">
@for (c of f.cells; track c.key) {
<sbo-input
[width]="c.width"
[maxlength]="c.kind === 'text' ? c.maxlength : undefined"
[ariaLabel]="c.label"
[readonly]="readOnly() || locked(f)"
[value]="str(header()[c.key])"
(valueChange)="setHeader(c.key, $event)"
/>
@if (c.kind === 'lookup' && !(readOnly() || locked(f))) {
<button type="button" class="sbo-btn" [attr.aria-label]="'Abrir la lista de ' + c.label.toLowerCase()" title="Lista de valores" (click)="groupCell.set(c)">…</button>
}
}
</sbo-field>
}
@case ('series') {
<sbo-field [label]="f.label">
<sbo-select
@@ -202,6 +202,7 @@ describe('DocumentFormPage', () => {
CardCode: 'C1',
DocDate: page().header().DocDate,
DocDueDate: page().header().DocDueDate,
TaxDate: page().header().TaxDate,
DiscountPercent: 0,
DocumentLines: [{ ItemCode: 'A1', Quantity: 1, Price: 50, DiscountPercent: 0 }],
});
@@ -210,6 +211,14 @@ describe('DocumentFormPage', () => {
expect(navigate).toHaveBeenCalledWith(['..', 99], expect.objectContaining({ state: { created: '1099' } }));
});
it('las tres fechas de un documento nuevo van juntas, una debajo de la otra: contabilización, vencimiento y del documento', () => {
const labels = page().layout.header.right.filter((f: { type: string }) => f.type === 'date').map((f: { label: string }) => f.label);
expect(labels).toEqual(['Fecha de contabilización', 'Fecha de vencimiento', 'Fecha del documento']);
const h = page().header();
expect(h.TaxDate).toBe(h.DocDate); // las tres arrancan con la fecha de hoy
expect(h.DocDueDate).toBe(h.DocDate);
});
it('al elegir el socio de un documento nuevo sin líneas se agrega una línea, y no se repite', async () => {
expect(page().lines()).toEqual([]);
page().onPartner({ CardCode: 'C1', CardName: 'ACME' });
@@ -34,7 +34,7 @@ import {
validateLayout,
} from './document-model';
import { SERVICE_DOC_TYPE } from './document-layouts';
import { type ComputedSource, type HeaderField, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasExpenses, hasFolio, hasSeries, headerKeysOf, keysOfFields, requiredHeaderOf } from './document-layouts';
import { type ComputedSource, type GroupCell, type HeaderField, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasExpenses, hasFolio, hasSeries, headerKeysOf, keysOfFields, requiredHeaderOf } from './document-layouts';
import { type CopyOrigin, copyFromDocument, copySources, copyTargets } from './document-copy';
import { DOCUMENT_SCREENS, type DocumentScreen, docNum, documentStatus, menuLabel } from './document-screens';
import { OBJECT_TYPES } from './document-objects';
@@ -447,6 +447,18 @@ export class DocumentFormPage {
/** Tipo al que se quiere cambiar y que espera confirmación porque hay líneas escritas que se descartarían. */
protected readonly pendingType = signal<string | null>(null);
/** La celda de un grupo de campos (el indicador) cuya lista de valores está abierta. */
protected readonly groupCell = signal<GroupCell | null>(null);
protected groupPicked(rows: ListRow[]): void {
const cell = this.groupCell();
const row = rows[0];
if (cell?.kind === 'lookup' && row) {
this.setHeader(cell.key, row[cell.lookup.valueKey ?? cell.lookup.columns[0]?.key ?? ''] ?? '');
}
this.groupCell.set(null);
}
/** Cambiar entre artículo y servicio cambia las columnas de línea: las líneas escritas no sirven y se pide permiso para descartarlas. */
private onDocType(type: string): void {
if (type === text(this.header()['DocType'])) return;
@@ -6,6 +6,14 @@ import { INDICATOR_LOOKUP, PROJECT_LOOKUP } from './dimension-lookups';
import { SERIES_SUBTYPE } from './document-objects';
import { ANALYTIC_COLUMNS, DOCUMENT_LINE_COLUMNS, lineColumnsFor, lineFieldsOf, newDocumentLine, newServiceLine, recalcLine, serviceLineColumns, uomEntryOf } from './line-columns';
/** Una celda de un grupo de campos: un texto (con ancho y largo máximo) o un código que se elige de una lista. */
export type GroupCell =
| { kind: 'text'; key: string; label: string; width?: string; maxlength?: number }
| { kind: 'lookup'; key: string; label: string; lookup: VhLookup; width?: string };
/** Fila con varios campos juntos, para la cabecera de un documento. */
export const fieldGroup = (label: string, cells: readonly GroupCell[]): HeaderField => ({ type: 'group', label, cells });
/** Valores calculados o del socio que una fila de solo lectura puede mostrar. */
export type ComputedSource = 'docNum' | 'partnerName' | 'partnerTaxId' | 'subtotal' | 'expenses' | 'tax' | 'total' | 'downPayment' | 'applied' | 'balance';
@@ -39,6 +47,11 @@ export type HeaderField =
| { type: 'partner'; key: string; label: string; required?: boolean; missing?: string }
/** Prefijo y número del folio tributario en una misma fila (`FolioPrefixString` y `FolioNumber`). */
| { type: 'folio'; label: string; prefixKey: string; numberKey: string }
/**
* Varios campos juntos en una sola fila, en el orden dado (el indicador, el prefijo y el número del folio). Una empresa
* que los sustituye por campos de usuario arma su propio grupo con `fieldGroup(...)` y oculta el del base.
*/
| { type: 'group'; label: string; cells: readonly GroupCell[] }
/** Serie de numeración y número del documento en una misma fila (`Series` y `DocNum`). */
| { type: 'series'; key: string; label: string }
/** Flete: código del gasto adicional y su importe (`DocumentAdditionalExpenses`). */
@@ -139,6 +152,16 @@ export const FIELDS = {
requester: { type: 'text', key: 'Requester', label: 'Solicitante (usuario)' },
} as const satisfies Record<string, HeaderField>;
/**
* Indicador y folio tributario juntos y en ese orden (en el Service Layer `Indicator`, `FolioPrefixString` y
* `FolioNumber`). El número se edita como texto y se envía como número.
*/
export const IDENTITY_GROUP: HeaderField = fieldGroup('Indicador / Folio', [
{ kind: 'lookup', key: 'Indicator', label: 'Indicador', lookup: INDICATOR_LOOKUP, width: '80px' },
{ kind: 'text', key: 'FolioPrefixString', label: 'Prefijo del folio', width: '90px', maxlength: 10 },
{ kind: 'text', key: 'FolioNumber', label: 'Número de folio' },
]);
export const commentsOnly: FieldColumns = { left: [FIELDS.comments], right: [] };
// --- columnas de línea compartidas ----------------------------------------------------------
@@ -237,6 +260,8 @@ export function marketingLayout(opts: {
uom: 'sales' | 'purchase';
/** Fila del flete en el pie (por defecto sí). */
expenses?: boolean;
/** Indicador y folio juntos en una fila de la derecha (facturas y notas de crédito) en lugar del indicador suelto a la izquierda y el folio. */
identity?: boolean;
/** Facturas y notas de crédito: el pie suma el anticipo total, el importe aplicado y el saldo pendiente. */
billing?: boolean;
/** Valores que se envían siempre al crear (ej. `DocumentSubType: 'bod_Bill'` en las boletas). */
@@ -254,11 +279,12 @@ export function marketingLayout(opts: {
{ type: 'computed', label: 'Nombre', source: 'partnerName' },
{ type: 'computed', label: 'RUT', source: 'partnerTaxId' },
FIELDS.numAtCard,
FIELDS.indicator,
...(opts.identity ? [] : [FIELDS.indicator]),
FIELDS.project,
...(opts.left ?? []),
],
right: [FIELDS.docNumber, FIELDS.docDate, FIELDS.docDueDate, ...(opts.right ?? [])],
// las tres fechas juntas, una debajo de la otra: contabilización, vencimiento y del documento
right: [FIELDS.docNumber, FIELDS.docDate, FIELDS.docDueDate, FIELDS.taxDate, ...(opts.identity ? [IDENTITY_GROUP] : []), ...(opts.right ?? [])],
},
footer: {
left: [...(opts.expenses === false ? [] : [{ type: 'expenses', label: 'Gastos adicionales' } as const]), FIELDS.comments],
@@ -277,7 +303,7 @@ export function marketingLayout(opts: {
],
},
columns,
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), TaxDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
newLine: newDocumentLine,
recalc: recalcLine,
service: {
@@ -309,6 +335,8 @@ export const keysOfFields = (fields: readonly HeaderField[]): string[] =>
return [];
case 'folio':
return [f.prefixKey, f.numberKey];
case 'group':
return f.cells.map((c) => c.key);
case 'expenses':
return [];
case 'rounding':
@@ -337,4 +365,4 @@ export const hasSeries = (layout: DocumentLayout): boolean => allFields(layout).
export const hasExpenses = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'expenses');
export const hasFolio = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'folio');
export const hasFolio = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'folio' || (f.type === 'group' && f.cells.some((c) => c.key === 'FolioNumber')));
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router, convertToParamMap, provideRouter } from '@angul
import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CLIENT_EXTENSIONS } from './client-extensions';
import { headerKeysOf } from './document-layouts';
import { fieldGroup, headerKeysOf } from './document-layouts';
import { lineFieldsOf } from './line-columns';
import { DocumentFormPage } from './document-form.page';
import { BASE_SCREENS, DOCUMENT_SCREENS } from './document-screens';
@@ -62,6 +62,44 @@ describe('extensiones por cliente', () => {
});
});
describe('indicador y folio juntos', () => {
it('la factura del base lleva Indicador, prefijo y número en una sola fila, en ese orden', () => {
const layout = by(BASE_SCREENS, '/ventas/facturas').layout;
const groups = layout.header.right.filter((f) => f.type === 'group');
expect(groups).toHaveLength(1);
expect(groups[0].type === 'group' && groups[0].cells.map((c) => c.key)).toEqual(['Indicator', 'FolioPrefixString', 'FolioNumber']);
expect(layout.header.left.some((f) => 'key' in f && f.key === 'Indicator')).toBe(false); // ya no va suelto
});
it('lo llevan facturas, boletas, reservas y notas de crédito de ventas y compras', () => {
for (const path of ['/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva', '/ventas/notas-credito', '/compras/facturas', '/compras/notas-credito']) {
expect(by(BASE_SCREENS, path).layout.header.right.some((f) => f.type === 'group'), path).toBe(true);
}
expect(by(BASE_SCREENS, '/ventas/ordenes').layout.header.right.some((f) => f.type === 'group')).toBe(false);
});
it('una empresa con campos de usuario los sustituye por su propio grupo, también juntos', () => {
const screens = applyExtensions(BASE_SCREENS, {
'/ventas/facturas': {
hideHeaderFields: ['Indicator'], // oculta el grupo completo del base
headerRight: [
fieldGroup('Tipo / Serie / Número', [
{ kind: 'text', key: 'U_Tipo', label: 'Tipo', width: '60px' },
{ kind: 'text', key: 'U_Serie', label: 'Serie', width: '80px' },
{ kind: 'text', key: 'U_Numero', label: 'Número' },
]),
],
},
});
const layout = by(screens, '/ventas/facturas').layout;
const groups = layout.header.right.filter((f) => f.type === 'group');
expect(groups).toHaveLength(1);
expect(groups[0].type === 'group' && groups[0].cells.map((c) => c.key)).toEqual(['U_Tipo', 'U_Serie', 'U_Numero']);
expect(headerKeysOf(layout)).toEqual(expect.arrayContaining(['U_Tipo', 'U_Serie', 'U_Numero']));
expect(headerKeysOf(layout)).not.toContain('FolioNumber');
});
});
describe('formulario con campos de cliente', () => {
let fixture: ComponentFixture<DocumentFormPage>;
let ctrl: HttpTestingController;
@@ -148,6 +148,8 @@ export interface MarketingExtras {
baseFilter?: string;
/** Documento de cobro o pago (factura, nota de crédito): el pie muestra anticipo, importe aplicado y saldo pendiente. */
billing?: boolean;
/** Indicador y folio juntos en una fila (`IDENTITY_GROUP`). */
identity?: boolean;
/** Valores que se envían siempre al crear (ej. `DocumentSubType`). */
fixed?: DocRecord;
/** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */
@@ -160,7 +162,7 @@ export interface MarketingExtras {
export const TAX_DOCUMENT: MarketingExtras = { headerRight: [FIELDS.folio] };
/** Facturas y notas de crédito: documento tributario que además se cobra o se paga. */
export const BILLING_DOCUMENT: MarketingExtras = { ...TAX_DOCUMENT, billing: true };
export const BILLING_DOCUMENT: MarketingExtras = { billing: true, identity: true };
/** Ventas y compras: documentos con socio de negocio y total. */
export const marketing = (
@@ -181,6 +183,7 @@ export const marketing = (
lookups: extras.lookups,
fixed: extras.fixed,
billing: extras.billing,
identity: extras.identity,
uom: path.startsWith('/ventas') ? 'sales' : 'purchase',
}),
resource,
@@ -26,8 +26,11 @@
</div>
<div class="sbo-hdr">
<div class="sbo-col">
<sbo-lookup-field label="Indicador" [lookup]="indicatorLookup" [value]="indicator()" (valueChange)="indicator.set($event)" [readonly]="saved()" />
<sbo-field label="Folio">
<sbo-field label="Indicador / Folio">
<sbo-input width="80px" ariaLabel="Indicador" [readonly]="saved()" [value]="indicator()" (valueChange)="indicator.set($event)" />
@if (!saved()) {
<button type="button" class="sbo-btn" aria-label="Abrir la lista de indicadores" title="Lista de valores" (click)="indicatorOpen.set(true)">…</button>
}
<sbo-input width="90px" [maxlength]="10" ariaLabel="Prefijo del folio" [readonly]="saved()" [value]="folioPrefix()" (valueChange)="set('folioPrefix', $event)" />
<sbo-input ariaLabel="Número de folio" [readonly]="saved()" [value]="folioNumber()" (valueChange)="set('folioNumber', $event)" />
</sbo-field>
@@ -76,3 +79,15 @@
}
<a sboActions class="btn" routerLink="..">{{ saved() ? 'Cerrar' : 'Descartar' }}</a>
</sbo-window>
@if (indicatorOpen()) {
<sbo-value-help
[open]="true"
[title]="indicatorLookup.title"
[resource]="indicatorLookup.resource"
[columns]="indicatorLookup.columns"
[orderby]="indicatorLookup.orderby"
(selected)="indicatorPicked($event)"
(closed)="indicatorOpen.set(false)"
/>
}
@@ -7,7 +7,9 @@ import { firstValueFrom, map } from 'rxjs';
import { API_URL, apiErrorMessage } from '../../core/api';
import { formatDate, formatNumber, todayIso } from '../../core/format';
import { SlDocumentApi } from '../../core/sl/sl-document';
import { DateField, LookupField, SboField, SelectField, TextField, TextInput } from '../../shared/form';
import { DateField, SboField, SelectField, TextField, TextInput } from '../../shared/form';
import type { ListRow } from '../../shared/list/list.types';
import { ValueHelp } from '../../shared/value-help/value-help';
import type { DetailColumn } from '../../shared/grid/grid.types';
import { RowsTable } from '../../shared/grid/rows-table';
import { Icon, SboWindow, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
@@ -57,7 +59,7 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
@Component({
selector: 'sbo-journal-form-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DateField, Icon, LookupField, RouterLink, RowsTable, SboField, SboWindow, SelectField, TextField, TextInput, Toolbar],
imports: [DateField, Icon, RouterLink, RowsTable, SboField, SboWindow, SelectField, TextField, TextInput, Toolbar, ValueHelp],
templateUrl: './journal-form.page.html',
styleUrl: './journal-form.page.css',
})
@@ -103,6 +105,13 @@ export class JournalFormPage {
protected readonly folioPrefix = linkedSignal(() => text(this.doc()?.['FolioPrefixString']));
protected readonly folioNumber = linkedSignal(() => text(this.doc()?.['FolioNumber']));
protected readonly indicatorLookup = INDICATOR_LOOKUP;
protected readonly indicatorOpen = signal(false);
protected indicatorPicked(rows: ListRow[]): void {
const row = rows[0];
if (row) this.indicator.set(String(row['IndicatorCode'] ?? ''));
this.indicatorOpen.set(false);
}
protected readonly lines = linkedSignal<Row[]>(() => {
const rows = ((this.doc()?.['JournalEntryLines'] as Row[] | undefined) ?? []).map((l) => ({ ...l }));
return this.doc() ? rows : [{ DueDate: today() }, { DueDate: today() }];