proposal-system/web/src/lib/format.ts

84 lines
2.4 KiB
TypeScript
Raw Normal View History

export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
}
export function formatBidAmount(amount: number): string {
return amount > 0 ? formatCurrency(amount) : 'Not priced';
}
export function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
export function formatDateTime(iso: string | null): string {
if (!iso) return '-';
return new Date(iso).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}
export function formatAge(iso: string): string {
const diffMs = Date.now() - new Date(iso).getTime();
const diffMinutes = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
const diffWeeks = Math.floor(diffDays / 7);
if (diffHours < 1) return `${Math.max(diffMinutes, 0)}m`;
if (diffDays < 1) return `${diffHours}h`;
if (diffDays < 7) return `${diffDays}d`;
return `${diffWeeks}w`;
}
export function getAgeColor(iso: string): string | undefined {
const diffDays = (Date.now() - new Date(iso).getTime()) / 86400000;
if (diffDays > 5) return '#d32f2f';
if (diffDays > 2) return '#ed6c02';
return undefined;
}
export interface GroupedProposal<T extends { proposalNumber: string }> {
latest: T;
revisions: T[];
hasRevisions: boolean;
baseNumber: string;
}
export function groupRevisions<T extends { proposalNumber: string }>(rows: T[]): GroupedProposal<T>[] {
const groups = new Map<string, T[]>();
for (const row of rows) {
const base = row.proposalNumber.replace(/-R\d+$/, '');
const existing = groups.get(base);
if (existing) {
existing.push(row);
} else {
groups.set(base, [row]);
}
}
const result: GroupedProposal<T>[] = [];
for (const [baseNumber, items] of groups) {
items.sort((a, b) => {
const revA = extractRevision(a.proposalNumber);
const revB = extractRevision(b.proposalNumber);
return revB - revA;
});
result.push({
latest: items[0]!,
revisions: items.slice(1),
hasRevisions: items.length > 1,
baseNumber,
});
}
return result;
}
function extractRevision(proposalNumber: string): number {
const match = proposalNumber.match(/-R(\d+)$/);
return match ? parseInt(match[1]!, 10) : 0;
}