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 { 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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user