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');