95 lines
2.8 KiB
TypeScript
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;
|
|
}
|