UDT-011: Localización Temporal Argentina (infra transversal) #25

Merged
dmolinari merged 24 commits from feature/UDT-011 into main 2026-04-18 13:57:49 +00:00
7 changed files with 38 additions and 58 deletions
Showing only changes of commit 03a02c63d5 - Show all commits

View File

@@ -5,15 +5,7 @@ import { CanPerform } from '@/components/auth/CanPerform'
import { useMedio } from '../hooks/useMedio' import { useMedio } from '../hooks/useMedio'
import { DeactivateMedioModal } from '../components/DeactivateMedioModal' import { DeactivateMedioModal } from '../components/DeactivateMedioModal'
import { tipoMedioLabel } from '../tipoMedio' import { tipoMedioLabel } from '../tipoMedio'
import { formatInstantOrDash } from '@/lib/dateFormat'
function formatDate(iso: string | null): string {
if (!iso) return '—'
return new Date(iso).toLocaleDateString('es-AR', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
export function MedioDetailPage() { export function MedioDetailPage() {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
@@ -69,11 +61,11 @@ export function MedioDetailPage() {
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Creado</span> <span className="text-muted-foreground">Creado</span>
<span>{formatDate(medio.fechaCreacion)}</span> <span>{formatInstantOrDash(medio.fechaCreacion)}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Modificado</span> <span className="text-muted-foreground">Modificado</span>
<span>{formatDate(medio.fechaModificacion)}</span> <span>{formatInstantOrDash(medio.fechaModificacion)}</span>
</div> </div>
</div> </div>

View File

@@ -7,15 +7,7 @@ import { useMedio } from '../../medios/hooks/useMedio'
import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal' import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal'
import { MedioInactivoBanner } from '../components/MedioInactivoBanner' import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
import { PdvInactivoBanner } from '../components/PdvInactivoBanner' import { PdvInactivoBanner } from '../components/PdvInactivoBanner'
import { formatInstantOrDash } from '@/lib/dateFormat'
function formatDate(iso: string | null): string {
if (!iso) return '—'
return new Date(iso).toLocaleDateString('es-AR', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
export function PuntoDeVentaDetailPage() { export function PuntoDeVentaDetailPage() {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
@@ -70,11 +62,11 @@ export function PuntoDeVentaDetailPage() {
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Creado</span> <span className="text-muted-foreground">Creado</span>
<span>{formatDate(pdv.fechaCreacion)}</span> <span>{formatInstantOrDash(pdv.fechaCreacion)}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Modificado</span> <span className="text-muted-foreground">Modificado</span>
<span>{formatDate(pdv.fechaModificacion)}</span> <span>{formatInstantOrDash(pdv.fechaModificacion)}</span>
</div> </div>
</div> </div>

View File

@@ -7,15 +7,7 @@ import { DeactivateSeccionModal } from '../components/DeactivateSeccionModal'
import { MedioInactivoBanner } from '../components/MedioInactivoBanner' import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
import { tipoSeccionLabel } from '../tipoSeccion' import { tipoSeccionLabel } from '../tipoSeccion'
import { useMedio } from '../../medios/hooks/useMedio' import { useMedio } from '../../medios/hooks/useMedio'
import { formatInstantOrDash } from '@/lib/dateFormat'
function formatDate(iso: string | null): string {
if (!iso) return '—'
return new Date(iso).toLocaleDateString('es-AR', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
export function SeccionDetailPage() { export function SeccionDetailPage() {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
@@ -75,11 +67,11 @@ export function SeccionDetailPage() {
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Creado</span> <span className="text-muted-foreground">Creado</span>
<span>{formatDate(seccion.fechaCreacion)}</span> <span>{formatInstantOrDash(seccion.fechaCreacion)}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">Modificado</span> <span className="text-muted-foreground">Modificado</span>
<span>{formatDate(seccion.fechaModificacion)}</span> <span>{formatInstantOrDash(seccion.fechaModificacion)}</span>
</div> </div>
</div> </div>

View File

@@ -3,21 +3,13 @@ import type { ColumnDef } from '@tanstack/react-table'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { DataTable } from '@/components/ui/data-table' import { DataTable } from '@/components/ui/data-table'
import type { UserListItem } from '../types' import type { UserListItem } from '../types'
import { formatInstantOrDash } from '@/lib/dateFormat'
interface UsersTableProps { interface UsersTableProps {
rows: UserListItem[] rows: UserListItem[]
onRowClick: (user: UserListItem) => void 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) { export function UsersTable({ rows, onRowClick }: UsersTableProps) {
const columns = useMemo<ColumnDef<UserListItem>[]>( const columns = useMemo<ColumnDef<UserListItem>[]>(
() => [ () => [
@@ -81,7 +73,7 @@ export function UsersTable({ rows, onRowClick }: UsersTableProps) {
header: 'Último login', header: 'Último login',
cell: ({ row }) => ( cell: ({ row }) => (
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{formatDate(row.original.ultimoLogin)} {formatInstantOrDash(row.original.ultimoLogin)}
</span> </span>
), ),
meta: { priority: 'low' }, meta: { priority: 'low' },

View File

@@ -87,6 +87,15 @@ export function parseCivilDate(yyyyMmDd: string): { year: number; month: number;
return { year: y, month: m, day: d }; 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". * 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. * Usa Date.UTC para aritmética pura — sin conversión de timezone en ningún momento.

View File

@@ -18,20 +18,7 @@ import {
toApiFilter, toApiFilter,
type AuditFiltersValue, type AuditFiltersValue,
} from './AuditFilters' } from './AuditFilters'
import { formatInstant } from '@/lib/dateFormat'
/** 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',
})
}
/** Copia texto al clipboard con fallback + toast. */ /** Copia texto al clipboard con fallback + toast. */
async function copyToClipboard(text: string, label: string): Promise<void> { async function copyToClipboard(text: string, label: string): Promise<void> {
@@ -123,7 +110,7 @@ export function AuditPage() {
header: 'Fecha', header: 'Fecha',
cell: ({ row }) => ( cell: ({ row }) => (
<span className="font-mono text-xs text-foreground"> <span className="font-mono text-xs text-foreground">
{formatOccurredAt(row.original.occurredAt)} {formatInstant(row.original.occurredAt)}
</span> </span>
), ),
meta: { priority: 'high' }, meta: { priority: 'high' },

View File

@@ -2,6 +2,7 @@ import { describe, it, expect, afterEach, vi } from 'vitest';
import { import {
AR_TZ, AR_TZ,
formatInstant, formatInstant,
formatInstantOrDash,
formatCivilDate, formatCivilDate,
formatCivilDateRange, formatCivilDateRange,
todayArgentina, 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)', () => { describe('formatCivilDate (Cat2 — yyyy-MM-dd → dd/MM/yyyy)', () => {
it('splits manually without new Date()', () => { it('splits manually without new Date()', () => {
expect(formatCivilDate('2026-05-01')).toBe('01/05/2026'); expect(formatCivilDate('2026-05-01')).toBe('01/05/2026');