← back to Govarbitrage

apps/mobile/lib/format.ts

126 lines

/**
 * Formatting utilities for financial data, scores, dates, and countdowns.
 */

// ── Currency ──────────────────────────────────────────────────────────────────

export function fmtUSD(value: number | null | undefined, decimals = 0): string {
  if (value == null || !Number.isFinite(value)) return "—";
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  }).format(value);
}

// Percentages above this ratio are treated as implausible/garbage and rendered
// as "—" instead of an absurd number (e.g. a backend annualizedReturn of
// 23002764 would otherwise display as 2,300,276,400.0%). 1000 ratio = 100,000%,
// generous enough to keep legitimate aggressive returns (e.g. roi 6.83 = 683%).
const MAX_PCT_RATIO = 1000;

export function fmtPct(
  value: number | null | undefined,
  opts?: { maxRatioAbs?: number; decimals?: number }
): string {
  if (value == null || !Number.isFinite(value)) return "—";
  // value is a decimal ratio (0.42 = 42%)
  const maxAbs = opts?.maxRatioAbs ?? MAX_PCT_RATIO;
  if (Math.abs(value) > maxAbs) return "—";
  const decimals = opts?.decimals ?? 1;
  return `${(value * 100).toFixed(decimals)}%`;
}

// ── Scores ────────────────────────────────────────────────────────────────────

export function fmtScore(score: number | null | undefined): string {
  if (score == null || !Number.isFinite(score)) return "—";
  return Math.round(score).toString();
}

// ── Time / countdown ──────────────────────────────────────────────────────────

/**
 * Returns a human-readable countdown string like "2d 4h" or "45m" or "CLOSED".
 * closingAt is an ISO8601 string.
 */
export function closingCountdown(closingAt: string | null | undefined): string {
  if (!closingAt) return "—";
  const ms = new Date(closingAt).getTime() - Date.now();
  if (ms <= 0) return "CLOSED";
  const totalMin = Math.floor(ms / 60_000);
  const days = Math.floor(totalMin / 1440);
  const hours = Math.floor((totalMin % 1440) / 60);
  const mins = totalMin % 60;
  if (days > 0) return `${days}d ${hours}h`;
  if (hours > 0) return `${hours}h ${mins}m`;
  return `${mins}m`;
}

export function isClosingSoon(closingAt: string | null | undefined): boolean {
  if (!closingAt) return false;
  const ms = new Date(closingAt).getTime() - Date.now();
  return ms > 0 && ms < 24 * 3_600_000; // within 24h
}

/**
 * Formats a date+time for admin cards in local timezone.
 * Steve's hard rule: admin cards must show created date AND time.
 */
export function fmtDateTime(iso: string | null | undefined): string {
  if (!iso) return "—";
  return new Date(iso).toLocaleString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });
}

// ── Condition / risk labels ───────────────────────────────────────────────────

export function conditionLabel(c: string | null): string {
  const map: Record<string, string> = {
    NEW: "New",
    LIKE_NEW: "Like New",
    USED_GOOD: "Used — Good",
    USED_FAIR: "Used — Fair",
    FOR_PARTS: "Parts Only",
    UNKNOWN: "Unknown",
  };
  return c ? (map[c] ?? c) : "Unknown";
}

export function sourceLabel(s: string): string {
  const map: Record<string, string> = {
    GOVDEALS: "GovDeals",
    GSA_AUCTIONS: "GSA Auctions",
    PUBLIC_SURPLUS: "Public Surplus",
    COUNTY: "County",
    STATE_SURPLUS: "State Surplus",
    UNIVERSITY_SURPLUS: "University Surplus",
    MUNICIBID: "Municibid",
    BID4ASSETS: "Bid4Assets",
    GOINDUSTRY: "GoIndustry",
    NETWORK_INTL: "Network Intl",
    GRAYS_AU: "Grays",
    GOVPLANET: "GovPlanet",
    CSV: "CSV Import",
    EXTENSION: "Extension",
    OTHER: "Other",
  };
  return map[s] ?? s;
}

export function dropShipLabel(d: string): string {
  const map: Record<string, string> = {
    EASY: "Drop Ship: Easy",
    MODERATE: "Drop Ship: OK",
    DIFFICULT: "Drop Ship: Hard",
    INFEASIBLE: "No Drop Ship",
  };
  return map[d] ?? d;
}