diff --git a/apps/web/src/app/core/format.spec.ts b/apps/web/src/app/core/format.spec.ts index ee52c53..29673a7 100644 --- a/apps/web/src/app/core/format.spec.ts +++ b/apps/web/src/app/core/format.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { formatDate, formatNumber, parseDecimal } from './format'; +import { formatDate, formatNumber, parseDecimal, todayIso } from './format'; describe('formatNumber', () => { it('importes con decimales fijos y miles', () => { @@ -38,3 +38,10 @@ describe('parseDecimal', () => { for (const v of ['', ' ', 'abc', '1.2.3', '--']) expect(parseDecimal(v), v).toBeNull(); }); }); + +describe('todayIso', () => { + it('da la fecha local, no la del día UTC', () => { + expect(todayIso(new Date(2026, 9, 19, 23, 30))).toBe('2026-10-19'); + expect(todayIso(new Date(2026, 0, 5, 0, 10))).toBe('2026-01-05'); + }); +}); diff --git a/apps/web/src/app/core/format.ts b/apps/web/src/app/core/format.ts index c471ee1..6f02d53 100644 --- a/apps/web/src/app/core/format.ts +++ b/apps/web/src/app/core/format.ts @@ -36,3 +36,9 @@ export function parseDecimal(text: string): number | null { const n = Number.parseFloat(cleaned); return Number.isFinite(n) ? n : null; } + +/** La fecha de hoy en la zona horaria del usuario, como `yyyy-MM-dd` (`toISOString` daría la del día UTC, que de noche es otra). */ +export function todayIso(now = new Date()): string { + const p = (n: number): string => String(n).padStart(2, '0'); + return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`; +} diff --git a/apps/web/src/app/features/banking/payment-form.page.ts b/apps/web/src/app/features/banking/payment-form.page.ts index 5e32948..10c73db 100644 --- a/apps/web/src/app/features/banking/payment-form.page.ts +++ b/apps/web/src/app/features/banking/payment-form.page.ts @@ -5,7 +5,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import type { SlListResponse } from '@basic-erp/sap-types'; import { map } from 'rxjs'; import { API_URL, apiErrorMessage } from '../../core/api'; -import { formatNumber } from '../../core/format'; +import { formatNumber, todayIso } from '../../core/format'; import { SlDocumentApi } from '../../core/sl/sl-document'; import { DateField, LookupField, NumberField, SelectField, TextField, TextareaField } from '../../shared/form'; import type { DetailColumn } from '../../shared/grid/grid.types'; @@ -21,7 +21,7 @@ import type { PaymentDocType, PaymentScreen } from './payment-types'; type Row = Record; const text = (v: unknown): string => (v === null || v === undefined ? '' : String(v)); -const today = (): string => new Date().toISOString().slice(0, 10); +const today = (): string => todayIso(); const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10) : ''); const money = (n: number): string => formatNumber(n, 2); diff --git a/apps/web/src/app/features/finance/journal-form.page.css b/apps/web/src/app/features/finance/journal-form.page.css index 22a76bc..3eca167 100644 --- a/apps/web/src/app/features/finance/journal-form.page.css +++ b/apps/web/src/app/features/finance/journal-form.page.css @@ -6,3 +6,4 @@ h3 { margin: 14px 0 4px; font-size: 13px; font-weight: 600; } .totals td:last-child { text-align: right; font-variant-numeric: tabular-nums; } .totals tr.strong td { font-weight: 600; border-top: 1px solid var(--field-b); } .confirm { color: var(--danger, #b00020); margin-right: 8px; } +.ask { display: flex; align-items: center; gap: 8px; margin: 4px 0 6px; padding: 4px 8px; background: var(--warn-bg); border: 1px solid var(--line); border-radius: var(--radius); } 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 0afb7e8..24546ea 100644 --- a/apps/web/src/app/features/finance/journal-form.page.html +++ b/apps/web/src/app/features/finance/journal-form.page.html @@ -20,7 +20,7 @@
- +
@@ -40,6 +40,13 @@

Líneas

+ @if (pendingDue(); as due) { + + } @if (!saved()) {

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 bf27a9c..bad11ab 100644 --- a/apps/web/src/app/features/finance/journal-form.page.ts +++ b/apps/web/src/app/features/finance/journal-form.page.ts @@ -5,7 +5,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import type { SlListResponse } from '@basic-erp/sap-types'; import { firstValueFrom, map } from 'rxjs'; import { API_URL, apiErrorMessage } from '../../core/api'; -import { formatNumber } from '../../core/format'; +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 type { DetailColumn } from '../../shared/grid/grid.types'; @@ -46,7 +46,7 @@ const ASSOCIATED_ACCOUNT_LOOKUP: VhLookup = { const OBJECT_TYPE = 30; const text = (v: unknown): string => (v === null || v === undefined ? '' : String(v)); -const today = (): string => new Date().toISOString().slice(0, 10); +const today = (): string => todayIso(); const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10) : ''); /** @@ -91,8 +91,9 @@ export class JournalFormPage { // --- el borrador (o el asiento guardado, mostrado) ----------------------------------------- protected readonly referenceDate = linkedSignal(() => (this.doc() ? dateOnly(this.doc()!['ReferenceDate']) : today())); - protected readonly dueDate = linkedSignal(() => dateOnly(this.doc()?.['DueDate'])); - protected readonly taxDate = linkedSignal(() => dateOnly(this.doc()?.['TaxDate'])); + // un asiento nuevo arranca con todas las fechas de hoy (cabecera y líneas) + protected readonly dueDate = linkedSignal(() => (this.doc() ? dateOnly(this.doc()!['DueDate']) : today())); + protected readonly taxDate = linkedSignal(() => (this.doc() ? dateOnly(this.doc()!['TaxDate']) : today())); protected readonly series = linkedSignal(() => (this.doc() ? Number(this.doc()!['Series']) || null : null)); protected readonly memo = linkedSignal(() => text(this.doc()?.['Memo'])); protected readonly reference = linkedSignal(() => text(this.doc()?.['Reference'])); @@ -104,7 +105,7 @@ export class JournalFormPage { protected readonly indicatorLookup = INDICATOR_LOOKUP; protected readonly lines = linkedSignal(() => { const rows = ((this.doc()?.['JournalEntryLines'] as Row[] | undefined) ?? []).map((l) => ({ ...l })); - return this.doc() ? rows : [{}, {}]; + return this.doc() ? rows : [{ DueDate: today() }, { DueDate: today() }]; }); protected readonly saving = signal(false); @@ -227,8 +228,26 @@ 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); + + 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); + } + + 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 addLine(): void { - this.setLines([...this.lines(), {}]); + this.setLines([...this.lines(), { DueDate: this.dueDate() }]); } /** Lleva a la última línea lo que falta para cuadrar: el debe o el haber, según el lado que sobra. */