From 68077fdf99f5ea8c8794fe25fe3e6733fe0ddfce Mon Sep 17 00:00:00 2001 From: George Date: Mon, 19 Oct 2026 11:59:58 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20asiento:=20referencias=201=20a=203?= =?UTF-8?q?=20y=20comentarios=20de=20la=20cabecera=20se=20ofrecen=20a=20la?= =?UTF-8?q?s=20l=C3=ADneas=20y=20las=20l=C3=ADneas=20nuevas=20los=20toman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../features/finance/journal-form.page.html | 8 ++-- .../app/features/finance/journal-form.page.ts | 48 +++++++++++++------ .../features/finance/journal-model.spec.ts | 11 ++++- .../src/app/features/finance/journal-model.ts | 27 +++++++++++ 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/apps/web/src/app/features/finance/journal-form.page.html b/apps/web/src/app/features/finance/journal-form.page.html index 24546ea..85f8ae4 100644 --- a/apps/web/src/app/features/finance/journal-form.page.html +++ b/apps/web/src/app/features/finance/journal-form.page.html @@ -40,11 +40,11 @@

Líneas

- @if (pendingDue(); as due) { + @for (p of pendingList(); track p.key) { } diff --git a/apps/web/src/app/features/finance/journal-form.page.ts b/apps/web/src/app/features/finance/journal-form.page.ts index bad11ab..043ef22 100644 --- a/apps/web/src/app/features/finance/journal-form.page.ts +++ b/apps/web/src/app/features/finance/journal-form.page.ts @@ -16,7 +16,7 @@ import { LineNamesService } from '../../core/sl/line-names'; import type { VhLookup } from '../../shared/value-help/value-help.types'; import { ACCOUNT_LOOKUP, INDICATOR_LOOKUP, PROJECT_LOOKUP, dimensionLookup } from '../documents/dimension-lookups'; import type { NavTarget } from '../documents/document-model'; -import { isManualJournal, journalOrigin, journalPayload, journalTotals, validateJournal } from './journal-model'; +import { HEADER_TO_LINE, type HeaderLineKey, isManualJournal, linesDiffer, newLineFrom, setOnLines, journalOrigin, journalPayload, journalTotals, validateJournal } from './journal-model'; type Row = Record; @@ -228,26 +228,43 @@ export class JournalFormPage { this.message.set(null); } - /** Vencimiento nuevo de la cabecera que espera la respuesta del usuario para pasarlo a las líneas. */ - protected readonly pendingDue = signal(null); + /** Lo que se cambió en la cabecera y espera la respuesta del usuario para pasarlo a las líneas. */ + protected readonly pending = signal>>({}); + protected readonly pendingList = computed(() => + HEADER_TO_LINE.flatMap((h) => { + const value = this.pending()[h.key]; + return value === undefined ? [] : [{ key: h.key, label: h.label, value: h.key === 'dueDate' ? formatDate(value) : value }]; + }), + ); - protected formatDate = formatDate; - - /** Al cambiar el vencimiento de la cabecera se pregunta si las líneas con otro vencimiento lo toman también. */ - protected onDueDate(value: string): void { - this.dueDate.set(value); - const differ = this.lines().some((l) => (l['DueDate'] ?? '') !== value); - this.pendingDue.set(value && differ ? value : null); + /** Al cambiar un dato de la cabecera que las líneas repiten, se pregunta si las que tienen otro valor lo toman también. */ + private offer(key: HeaderLineKey, value: string): void { + const line = HEADER_TO_LINE.find((h) => h.key === key)!.line; + this.pending.update((p) => { + const next = { ...p }; + if (linesDiffer(this.lines(), line, value)) next[key] = value; + else delete next[key]; + return next; + }); } - protected applyDue(yes: boolean): void { - const value = this.pendingDue(); - this.pendingDue.set(null); - if (yes && value) this.setLines(this.lines().map((l) => ({ ...l, DueDate: value }))); + protected onDueDate(value: string): void { + this.dueDate.set(value); + this.offer('dueDate', value); + } + + protected applyPending(key: HeaderLineKey, yes: boolean): void { + const value = this.pending()[key]; + this.pending.update((p) => { + const next = { ...p }; + delete next[key]; + return next; + }); + if (yes && value !== undefined) this.setLines(setOnLines(this.lines(), HEADER_TO_LINE.find((h) => h.key === key)!.line, value)); } protected addLine(): void { - this.setLines([...this.lines(), { DueDate: this.dueDate() }]); + this.setLines([...this.lines(), newLineFrom({ dueDate: this.dueDate(), memo: this.memo(), reference: this.reference(), reference2: this.reference2(), reference3: this.reference3() })]); } /** Lleva a la última línea lo que falta para cuadrar: el debe o el haber, según el lado que sobra. */ @@ -272,6 +289,7 @@ export class JournalFormPage { protected set(key: 'memo' | 'reference' | 'reference2' | 'reference3' | 'folioPrefix' | 'folioNumber', value: string): void { this[key].set(value); this.message.set(null); + if (key === 'memo' || key === 'reference' || key === 'reference2' || key === 'reference3') this.offer(key, value); } protected async go(target: NavTarget): Promise { diff --git a/apps/web/src/app/features/finance/journal-model.spec.ts b/apps/web/src/app/features/finance/journal-model.spec.ts index 57c9274..aec14dd 100644 --- a/apps/web/src/app/features/finance/journal-model.spec.ts +++ b/apps/web/src/app/features/finance/journal-model.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { type JournalDraft, isManualJournal, journalOrigin, journalPayload, journalTotals, validateJournal } from './journal-model'; +import { type JournalDraft, isManualJournal, linesDiffer, newLineFrom, setOnLines, journalOrigin, journalPayload, journalTotals, validateJournal } from './journal-model'; const draft = (over: Partial = {}): JournalDraft => ({ referenceDate: '2026-10-16', @@ -91,4 +91,13 @@ describe('asiento contable', () => { expect(journalOrigin('ttARInvoice')).toBe('Factura de venta'); expect(journalOrigin('ttRaro')).toBe('ttRaro'); }); + + it('lo que repiten las líneas: se ofrece si alguna difiere y una línea nueva lo toma de la cabecera', () => { + const lines = [{ Reference1: 'A' }, { Reference1: 'B' }, {}]; + expect(linesDiffer(lines, 'Reference1', 'A')).toBe(true); + expect(linesDiffer([{ Reference1: 'A' }, { Reference1: 'A' }], 'Reference1', 'A')).toBe(false); + expect(linesDiffer(lines, 'Reference1', '')).toBe(false); // un valor vacío no se ofrece + expect(setOnLines(lines, 'Reference1', 'Z').map((l) => l['Reference1'])).toEqual(['Z', 'Z', 'Z']); + expect(newLineFrom({ dueDate: '2026-10-19', memo: 'Ajuste', reference: '', reference2: 'R2', reference3: '' })).toEqual({ DueDate: '2026-10-19', LineMemo: 'Ajuste', Reference2: 'R2' }); + }); }); diff --git a/apps/web/src/app/features/finance/journal-model.ts b/apps/web/src/app/features/finance/journal-model.ts index 7b70319..64c7114 100644 --- a/apps/web/src/app/features/finance/journal-model.ts +++ b/apps/web/src/app/features/finance/journal-model.ts @@ -111,3 +111,30 @@ const ORIGINS: Readonly> = { }; export const journalOrigin = (code: unknown): string => ORIGINS[String(code ?? '')] ?? String(code ?? ''); + +/** + * Datos de la cabecera que las líneas repiten: al cambiarlos se ofrece pasarlos a las líneas y una línea nueva + * los toma. Una línea que se edita no cambia la cabecera: solo va de la cabecera hacia las líneas. + */ +export const HEADER_TO_LINE = [ + { key: 'dueDate', line: 'DueDate', label: 'el vencimiento' }, + { key: 'memo', line: 'LineMemo', label: 'los comentarios' }, + { key: 'reference', line: 'Reference1', label: 'la referencia 1' }, + { key: 'reference2', line: 'Reference2', label: 'la referencia 2' }, + { key: 'reference3', line: 'AdditionalReference', label: 'la referencia 3' }, +] as const; + +export type HeaderLineKey = (typeof HEADER_TO_LINE)[number]['key']; + +/** Alguna línea tiene otro valor que `value` en ese campo (y hay algo que ofrecer: un valor vacío no se ofrece). */ +export const linesDiffer = (lines: readonly Row[], lineKey: string, value: string): boolean => has(value) && lines.some((l) => String(l[lineKey] ?? '') !== value); + +/** Las líneas con ese campo puesto a `value`. */ +export const setOnLines = (lines: readonly Row[], lineKey: string, value: string): Row[] => lines.map((l) => ({ ...l, [lineKey]: value })); + +/** Una línea nueva con lo que tiene la cabecera. */ +export function newLineFrom(header: Readonly>): Row { + const line: Row = {}; + for (const h of HEADER_TO_LINE) if (has(header[h.key])) line[h.line] = header[h.key]; + return line; +}