feat(web): asiento: referencias 1 a 3 y comentarios de la cabecera se ofrecen a las líneas y las líneas nuevas los toman
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba0ab025de
commit
68077fdf99
4 files changed
+74
-20
No files matched your search
@@ -40,11 +40,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Líneas</h3>
|
<h3>Líneas</h3>
|
||||||
@if (pendingDue(); as due) {
|
@for (p of pendingList(); track p.key) {
|
||||||
<p class="ask" role="alert">
|
<p class="ask" role="alert">
|
||||||
¿Poner también el vencimiento {{ formatDate(due) }} en las líneas?
|
¿Poner también {{ p.label }} «{{ p.value }}» en las líneas?
|
||||||
<button type="button" class="btn main" (click)="applyDue(true)">Sí, actualizar</button>
|
<button type="button" class="btn main" (click)="applyPending(p.key, true)">Sí, actualizar</button>
|
||||||
<button type="button" class="btn" (click)="applyDue(false)">No, dejarlas</button>
|
<button type="button" class="btn" (click)="applyPending(p.key, false)">No, dejarlas</button>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<sbo-rows-table [columns]="columns" [rows]="lines()" (rowsChange)="setLines($event)" [readonly]="saved()" [removable]="!saved()" emptyText="Sin líneas." />
|
<sbo-rows-table [columns]="columns" [rows]="lines()" (rowsChange)="setLines($event)" [readonly]="saved()" [removable]="!saved()" emptyText="Sin líneas." />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { LineNamesService } from '../../core/sl/line-names';
|
|||||||
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
||||||
import { ACCOUNT_LOOKUP, INDICATOR_LOOKUP, PROJECT_LOOKUP, dimensionLookup } from '../documents/dimension-lookups';
|
import { ACCOUNT_LOOKUP, INDICATOR_LOOKUP, PROJECT_LOOKUP, dimensionLookup } from '../documents/dimension-lookups';
|
||||||
import type { NavTarget } from '../documents/document-model';
|
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<string, unknown>;
|
type Row = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -228,26 +228,43 @@ export class JournalFormPage {
|
|||||||
this.message.set(null);
|
this.message.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vencimiento nuevo de la cabecera que espera la respuesta del usuario para pasarlo a las líneas. */
|
/** Lo que se cambió en la cabecera y espera la respuesta del usuario para pasarlo a las líneas. */
|
||||||
protected readonly pendingDue = signal<string | null>(null);
|
protected readonly pending = signal<Partial<Record<HeaderLineKey, string>>>({});
|
||||||
|
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 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 {
|
||||||
/** Al cambiar el vencimiento de la cabecera se pregunta si las líneas con otro vencimiento lo toman también. */
|
const line = HEADER_TO_LINE.find((h) => h.key === key)!.line;
|
||||||
protected onDueDate(value: string): void {
|
this.pending.update((p) => {
|
||||||
this.dueDate.set(value);
|
const next = { ...p };
|
||||||
const differ = this.lines().some((l) => (l['DueDate'] ?? '') !== value);
|
if (linesDiffer(this.lines(), line, value)) next[key] = value;
|
||||||
this.pendingDue.set(value && differ ? value : null);
|
else delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected applyDue(yes: boolean): void {
|
protected onDueDate(value: string): void {
|
||||||
const value = this.pendingDue();
|
this.dueDate.set(value);
|
||||||
this.pendingDue.set(null);
|
this.offer('dueDate', value);
|
||||||
if (yes && value) this.setLines(this.lines().map((l) => ({ ...l, 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 {
|
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. */
|
/** 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 {
|
protected set(key: 'memo' | 'reference' | 'reference2' | 'reference3' | 'folioPrefix' | 'folioNumber', value: string): void {
|
||||||
this[key].set(value);
|
this[key].set(value);
|
||||||
this.message.set(null);
|
this.message.set(null);
|
||||||
|
if (key === 'memo' || key === 'reference' || key === 'reference2' || key === 'reference3') this.offer(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async go(target: NavTarget): Promise<void> {
|
protected async go(target: NavTarget): Promise<void> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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> = {}): JournalDraft => ({
|
const draft = (over: Partial<JournalDraft> = {}): JournalDraft => ({
|
||||||
referenceDate: '2026-10-16',
|
referenceDate: '2026-10-16',
|
||||||
@@ -91,4 +91,13 @@ describe('asiento contable', () => {
|
|||||||
expect(journalOrigin('ttARInvoice')).toBe('Factura de venta');
|
expect(journalOrigin('ttARInvoice')).toBe('Factura de venta');
|
||||||
expect(journalOrigin('ttRaro')).toBe('ttRaro');
|
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' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -111,3 +111,30 @@ const ORIGINS: Readonly<Record<string, string>> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const journalOrigin = (code: unknown): string => ORIGINS[String(code ?? '')] ?? String(code ?? '');
|
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<Record<HeaderLineKey, string>>): Row {
|
||||||
|
const line: Row = {};
|
||||||
|
for (const h of HEADER_TO_LINE) if (has(header[h.key])) line[h.line] = header[h.key];
|
||||||
|
return line;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user