Files
ok_r/lib/kr-utils.ts
2026-01-25 21:55:29 +08:00

95 lines
2.8 KiB
TypeScript

import type { KR, ProjectStatus, Checkin } from '@/types/database';
export function getTodayInTokyo(): string {
const now = new Date();
const tokyoTime = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Tokyo' }));
return tokyoTime.toISOString().split('T')[0];
}
export function calculateProgress(kr: KR): number {
if (kr.direction === 'increase') {
return kr.target_value > 0 ? kr.current_value / kr.target_value : 0;
} else {
return kr.current_value > 0 ? kr.target_value / kr.current_value : 0;
}
}
export function calculateExpectedProgress(kr: KR): number {
const today = getTodayInTokyo();
const start = new Date(kr.period_start);
const end = new Date(kr.period_end);
const current = new Date(today);
const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
const elapsedDays = Math.max(0, Math.ceil((current.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1);
return Math.min(1, elapsedDays / totalDays);
}
export function determineKRStatus(
kr: KR,
recentCheckins: Checkin[] = []
): ProjectStatus {
const progress = calculateProgress(kr);
const expectedProgress = calculateExpectedProgress(kr);
const progressGap = expectedProgress - progress;
const consecutiveYellows = recentCheckins
.slice(0, 2)
.filter(c => c.status_snapshot === 'yellow').length >= 2;
if (consecutiveYellows) {
return 'red';
}
if (kr.confidence < 50 || progressGap > 0.15) {
return 'red';
}
if ((kr.confidence >= 50 && kr.confidence < 80) || (progressGap > 0.05 && progressGap <= 0.15)) {
return 'yellow';
}
if (kr.confidence >= 80 && progressGap <= 0.05) {
return 'green';
}
return 'yellow';
}
export function determineProjectStatus(krs: KR[]): ProjectStatus {
if (krs.length === 0) return 'green';
const redCount = krs.filter(kr => kr.status === 'red').length;
const yellowCount = krs.filter(kr => kr.status === 'yellow').length;
if (redCount > 0) return 'red';
const yellowRatio = yellowCount / krs.length;
if (yellowRatio > 0.3) return 'red';
if (yellowCount > 0) return 'yellow';
return 'green';
}
export function formatDateRange(start: string, end: string): string {
const startDate = new Date(start);
const endDate = new Date(end);
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
return `${startDate.toLocaleDateString('zh-CN', formatOptions)} - ${endDate.toLocaleDateString('zh-CN', formatOptions)}`;
}
export function getDaysRemaining(endDate: string): number {
const today = new Date(getTodayInTokyo());
const end = new Date(endDate);
return Math.ceil((end.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
}
export function isKRActive(kr: KR): boolean {
const today = getTodayInTokyo();
return kr.period_start <= today && kr.period_end >= today;
}