UDT-011: Localización Temporal Argentina (infra transversal) #25
@@ -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() {
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Creado</span>
|
||||
<span>{formatDate(medio.fechaCreacion)}</span>
|
||||
<span>{formatInstantOrDash(medio.fechaCreacion)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Modificado</span>
|
||||
<span>{formatDate(medio.fechaModificacion)}</span>
|
||||
<span>{formatInstantOrDash(medio.fechaModificacion)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Creado</span>
|
||||
<span>{formatDate(pdv.fechaCreacion)}</span>
|
||||
<span>{formatInstantOrDash(pdv.fechaCreacion)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Modificado</span>
|
||||
<span>{formatDate(pdv.fechaModificacion)}</span>
|
||||
<span>{formatInstantOrDash(pdv.fechaModificacion)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Creado</span>
|
||||
<span>{formatDate(seccion.fechaCreacion)}</span>
|
||||
<span>{formatInstantOrDash(seccion.fechaCreacion)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Modificado</span>
|
||||
<span>{formatDate(seccion.fechaModificacion)}</span>
|
||||
<span>{formatInstantOrDash(seccion.fechaModificacion)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<ColumnDef<UserListItem>[]>(
|
||||
() => [
|
||||
@@ -81,7 +73,7 @@ export function UsersTable({ rows, onRowClick }: UsersTableProps) {
|
||||
header: 'Último login',
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">
|
||||
{formatDate(row.original.ultimoLogin)}
|
||||
{formatInstantOrDash(row.original.ultimoLogin)}
|
||||
</span>
|
||||
),
|
||||
meta: { priority: 'low' },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -123,7 +110,7 @@ export function AuditPage() {
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono text-xs text-foreground">
|
||||
{formatOccurredAt(row.original.occurredAt)}
|
||||
{formatInstant(row.original.occurredAt)}
|
||||
</span>
|
||||
),
|
||||
meta: { priority: 'high' },
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user