From 03a02c63d5b92b4d88a0f13abfac09aa63ff2c35 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 10:26:29 -0300 Subject: [PATCH] refactor(web/udt-011): eliminar 4 funciones formatDate duplicadas y formatOccurredAt, usar dateFormat utility (fix BUG-FE-01, BUG-FE-02) --- .../features/medios/pages/MedioDetailPage.tsx | 14 +++----------- .../pages/PuntoDeVentaDetailPage.tsx | 14 +++----------- .../secciones/pages/SeccionDetailPage.tsx | 14 +++----------- .../features/users/components/UsersTable.tsx | 12 ++---------- src/web/src/lib/dateFormat.ts | 9 +++++++++ src/web/src/pages/admin/audit/AuditPage.tsx | 17 ++--------------- src/web/src/tests/lib/dateFormat.test.ts | 16 ++++++++++++++++ 7 files changed, 38 insertions(+), 58 deletions(-) diff --git a/src/web/src/features/medios/pages/MedioDetailPage.tsx b/src/web/src/features/medios/pages/MedioDetailPage.tsx index 2992e68..468f201 100644 --- a/src/web/src/features/medios/pages/MedioDetailPage.tsx +++ b/src/web/src/features/medios/pages/MedioDetailPage.tsx @@ -5,15 +5,7 @@ import { CanPerform } from '@/components/auth/CanPerform' import { useMedio } from '../hooks/useMedio' import { DeactivateMedioModal } from '../components/DeactivateMedioModal' import { tipoMedioLabel } from '../tipoMedio' - -function formatDate(iso: string | null): string { - if (!iso) return '—' - return new Date(iso).toLocaleDateString('es-AR', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) -} +import { formatInstantOrDash } from '@/lib/dateFormat' export function MedioDetailPage() { const { id } = useParams<{ id: string }>() @@ -69,11 +61,11 @@ export function MedioDetailPage() {
Creado - {formatDate(medio.fechaCreacion)} + {formatInstantOrDash(medio.fechaCreacion)}
Modificado - {formatDate(medio.fechaModificacion)} + {formatInstantOrDash(medio.fechaModificacion)}
diff --git a/src/web/src/features/puntos-de-venta/pages/PuntoDeVentaDetailPage.tsx b/src/web/src/features/puntos-de-venta/pages/PuntoDeVentaDetailPage.tsx index f948c47..ec805f4 100644 --- a/src/web/src/features/puntos-de-venta/pages/PuntoDeVentaDetailPage.tsx +++ b/src/web/src/features/puntos-de-venta/pages/PuntoDeVentaDetailPage.tsx @@ -7,15 +7,7 @@ import { useMedio } from '../../medios/hooks/useMedio' import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal' import { MedioInactivoBanner } from '../components/MedioInactivoBanner' import { PdvInactivoBanner } from '../components/PdvInactivoBanner' - -function formatDate(iso: string | null): string { - if (!iso) return '—' - return new Date(iso).toLocaleDateString('es-AR', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) -} +import { formatInstantOrDash } from '@/lib/dateFormat' export function PuntoDeVentaDetailPage() { const { id } = useParams<{ id: string }>() @@ -70,11 +62,11 @@ export function PuntoDeVentaDetailPage() {
Creado - {formatDate(pdv.fechaCreacion)} + {formatInstantOrDash(pdv.fechaCreacion)}
Modificado - {formatDate(pdv.fechaModificacion)} + {formatInstantOrDash(pdv.fechaModificacion)}
diff --git a/src/web/src/features/secciones/pages/SeccionDetailPage.tsx b/src/web/src/features/secciones/pages/SeccionDetailPage.tsx index 5a5e448..7c3f846 100644 --- a/src/web/src/features/secciones/pages/SeccionDetailPage.tsx +++ b/src/web/src/features/secciones/pages/SeccionDetailPage.tsx @@ -7,15 +7,7 @@ import { DeactivateSeccionModal } from '../components/DeactivateSeccionModal' import { MedioInactivoBanner } from '../components/MedioInactivoBanner' import { tipoSeccionLabel } from '../tipoSeccion' import { useMedio } from '../../medios/hooks/useMedio' - -function formatDate(iso: string | null): string { - if (!iso) return '—' - return new Date(iso).toLocaleDateString('es-AR', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) -} +import { formatInstantOrDash } from '@/lib/dateFormat' export function SeccionDetailPage() { const { id } = useParams<{ id: string }>() @@ -75,11 +67,11 @@ export function SeccionDetailPage() {
Creado - {formatDate(seccion.fechaCreacion)} + {formatInstantOrDash(seccion.fechaCreacion)}
Modificado - {formatDate(seccion.fechaModificacion)} + {formatInstantOrDash(seccion.fechaModificacion)}
diff --git a/src/web/src/features/users/components/UsersTable.tsx b/src/web/src/features/users/components/UsersTable.tsx index 059e137..a9c0378 100644 --- a/src/web/src/features/users/components/UsersTable.tsx +++ b/src/web/src/features/users/components/UsersTable.tsx @@ -3,21 +3,13 @@ import type { ColumnDef } from '@tanstack/react-table' import { Badge } from '@/components/ui/badge' import { DataTable } from '@/components/ui/data-table' import type { UserListItem } from '../types' +import { formatInstantOrDash } from '@/lib/dateFormat' interface UsersTableProps { rows: UserListItem[] onRowClick: (user: UserListItem) => void } -function formatDate(iso: string | null): string { - if (!iso) return '—' - return new Date(iso).toLocaleDateString('es-AR', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) -} - export function UsersTable({ rows, onRowClick }: UsersTableProps) { const columns = useMemo[]>( () => [ @@ -81,7 +73,7 @@ export function UsersTable({ rows, onRowClick }: UsersTableProps) { header: 'Último login', cell: ({ row }) => ( - {formatDate(row.original.ultimoLogin)} + {formatInstantOrDash(row.original.ultimoLogin)} ), meta: { priority: 'low' }, diff --git a/src/web/src/lib/dateFormat.ts b/src/web/src/lib/dateFormat.ts index d757788..e436dd6 100644 --- a/src/web/src/lib/dateFormat.ts +++ b/src/web/src/lib/dateFormat.ts @@ -87,6 +87,15 @@ export function parseCivilDate(yyyyMmDd: string): { year: number; month: number; return { year: y, month: m, day: d }; } +/** + * Formatea un instante UTC (Cat1) nullable a string legible en zona horaria Argentina. + * Retorna '—' cuando el valor es null o undefined. + */ +export function formatInstantOrDash(iso: string | null | undefined): string { + if (!iso) return '—'; + return formatInstant(iso); +} + /** * Retorna el día anterior a una fecha civil Argentina en formato "yyyy-MM-dd". * Usa Date.UTC para aritmética pura — sin conversión de timezone en ningún momento. diff --git a/src/web/src/pages/admin/audit/AuditPage.tsx b/src/web/src/pages/admin/audit/AuditPage.tsx index b16e960..7979ac4 100644 --- a/src/web/src/pages/admin/audit/AuditPage.tsx +++ b/src/web/src/pages/admin/audit/AuditPage.tsx @@ -18,20 +18,7 @@ import { toApiFilter, type AuditFiltersValue, } from './AuditFilters' - -/** Formatea un ISO datetime a hora local AR (dd/mm/yyyy HH:mm:ss). */ -function formatOccurredAt(iso: string): string { - const d = new Date(iso) - if (Number.isNaN(d.getTime())) return iso - return d.toLocaleString('es-AR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }) -} +import { formatInstant } from '@/lib/dateFormat' /** Copia texto al clipboard con fallback + toast. */ async function copyToClipboard(text: string, label: string): Promise { @@ -123,7 +110,7 @@ export function AuditPage() { header: 'Fecha', cell: ({ row }) => ( - {formatOccurredAt(row.original.occurredAt)} + {formatInstant(row.original.occurredAt)} ), meta: { priority: 'high' }, diff --git a/src/web/src/tests/lib/dateFormat.test.ts b/src/web/src/tests/lib/dateFormat.test.ts index a7e4624..f428760 100644 --- a/src/web/src/tests/lib/dateFormat.test.ts +++ b/src/web/src/tests/lib/dateFormat.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, afterEach, vi } from 'vitest'; import { AR_TZ, formatInstant, + formatInstantOrDash, formatCivilDate, formatCivilDateRange, todayArgentina, @@ -33,6 +34,21 @@ describe('dateFormat.ts', () => { }); }); + describe('formatInstantOrDash (Cat1 nullable)', () => { + it('returns "—" for null', () => { + expect(formatInstantOrDash(null)).toBe('—'); + }); + + it('returns "—" for undefined', () => { + expect(formatInstantOrDash(undefined)).toBe('—'); + }); + + it('delegates to formatInstant for valid ISO', () => { + const iso = '2026-05-01T01:30:00.000Z'; + expect(formatInstantOrDash(iso)).toBe(formatInstant(iso)); + }); + }); + describe('formatCivilDate (Cat2 — yyyy-MM-dd → dd/MM/yyyy)', () => { it('splits manually without new Date()', () => { expect(formatCivilDate('2026-05-01')).toBe('01/05/2026');