File size: 4,058 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
 * Timeline helpers — pure functions for grouping AuditLogEntry arrays by day
 * and producing human-readable relative timestamps.
 *
 * No I/O, no side-effects — safe to import in both server and client contexts.
 */

import type { AuditLogEntry } from "@/lib/compliance/index";

export interface DayGroup {
  /** YYYY-MM-DD in local server time */
  dayKey: string;
  /** "today" | "yesterday" | ISO date string for older days */
  label: "today" | "yesterday" | string;
  entries: AuditLogEntry[];
}

/**
 * Returns YYYY-MM-DD for a given ISO timestamp (using local server time).
 */
function toDayKey(iso: string): string {
  const d = new Date(iso);
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

/**
 * Returns YYYY-MM-DD for a given epoch ms (local server time).
 */
function epochToDayKey(ms: number): string {
  const d = new Date(ms);
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

/**
 * Groups audit log entries by calendar day (local server time), sorted
 * descending (most recent day first). Each group has a human label:
 * - "today" for the current day
 * - "yesterday" for the previous day
 * - ISO date string "YYYY-MM-DD" for older days
 *
 * @param entries - Flat list of audit entries (order not assumed)
 * @param referenceNowMs - Override for "now" (ms since epoch). Defaults to Date.now().
 */
export function groupByDay(entries: AuditLogEntry[], referenceNowMs?: number): DayGroup[] {
  if (!entries.length) return [];

  const nowMs = referenceNowMs ?? Date.now();
  const todayKey = epochToDayKey(nowMs);
  const yesterdayKey = epochToDayKey(nowMs - 24 * 60 * 60 * 1000);

  // Sort descending by timestamp
  const sorted = [...entries].sort((a, b) => {
    const ta = a.timestamp ?? "";
    const tb = b.timestamp ?? "";
    if (ta > tb) return -1;
    if (ta < tb) return 1;
    // tiebreak by id desc
    const ia = typeof a.id === "number" ? a.id : 0;
    const ib = typeof b.id === "number" ? b.id : 0;
    return ib - ia;
  });

  const groupMap = new Map<string, AuditLogEntry[]>();
  const dayOrder: string[] = [];

  for (const entry of sorted) {
    const dk = toDayKey(entry.timestamp ?? "");
    if (!groupMap.has(dk)) {
      groupMap.set(dk, []);
      dayOrder.push(dk);
    }
    groupMap.get(dk)!.push(entry);
  }

  return dayOrder.map((dk) => {
    let label: string;
    if (dk === todayKey) {
      label = "today";
    } else if (dk === yesterdayKey) {
      label = "yesterday";
    } else {
      label = dk;
    }
    return { dayKey: dk, label, entries: groupMap.get(dk)! };
  });
}

/**
 * Returns a human-readable relative time string for the given ISO timestamp.
 *
 * @param iso - ISO 8601 timestamp string
 * @param locale - "en" or "pt-BR"
 * @param referenceNowMs - Override for "now" (ms since epoch). Defaults to Date.now().
 */
export function relativeTime(
  iso: string,
  locale: "en" | "pt-BR",
  referenceNowMs?: number
): string {
  const nowMs = referenceNowMs ?? Date.now();
  const then = new Date(iso).getTime();
  if (!Number.isFinite(then)) {
    return locale === "pt-BR" ? "agora há pouco" : "just now";
  }

  const diffMs = nowMs - then;
  const diffSec = Math.floor(diffMs / 1000);
  const diffMin = Math.floor(diffSec / 60);
  const diffHour = Math.floor(diffMin / 60);
  const diffDay = Math.floor(diffHour / 24);

  if (locale === "pt-BR") {
    if (diffSec < 60) return "agora há pouco";
    if (diffMin < 60) return `há ${diffMin} min`;
    if (diffHour < 24) return `há ${diffHour} h`;
    if (diffDay === 1) return "ontem";
    return `há ${diffDay} dias`;
  }

  // English
  if (diffSec < 60) return "just now";
  if (diffMin < 60) return `${diffMin} min ago`;
  if (diffHour < 24) return `${diffHour} h ago`;
  if (diffDay === 1) return "yesterday";
  return `${diffDay} days ago`;
}