fix: force dynamic for meeting page
This commit is contained in:
33
lib/constants.ts
Normal file
33
lib/constants.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const REASON_CODES = {
|
||||
RESOURCE: '资源不足',
|
||||
DEPENDENCY: '依赖阻塞',
|
||||
DECISION: '决策延迟',
|
||||
SCOPE: '范围变更',
|
||||
EXECUTION: '执行问题',
|
||||
OTHER: '其他原因',
|
||||
} as const;
|
||||
|
||||
export const PERIOD_TYPES = {
|
||||
custom: '自定义',
|
||||
'3d': '3天',
|
||||
'1w': '1周',
|
||||
'2w': '2周',
|
||||
monthly: '月度',
|
||||
} as const;
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
green: { bg: 'bg-emerald-100', text: 'text-emerald-700', border: 'border-emerald-500', label: '正常' },
|
||||
yellow: { bg: 'bg-amber-100', text: 'text-amber-700', border: 'border-amber-500', label: '警告' },
|
||||
red: { bg: 'bg-rose-100', text: 'text-rose-700', border: 'border-rose-500', label: '风险' },
|
||||
} as const;
|
||||
|
||||
export const ROLE_LABELS = {
|
||||
owner: '老板',
|
||||
pm: '项目负责人',
|
||||
member: '成员',
|
||||
} as const;
|
||||
|
||||
export const DIRECTION_LABELS = {
|
||||
increase: '增加',
|
||||
decrease: '减少',
|
||||
} as const;
|
||||
27
lib/error-utils.ts
Normal file
27
lib/error-utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string') return error;
|
||||
|
||||
const message = error instanceof Error ? error.message : '未知错误';
|
||||
|
||||
// Postgres/Supabase errors
|
||||
if (message.includes('invalid input syntax for type uuid')) {
|
||||
return 'ID 格式错误 (UUID)';
|
||||
}
|
||||
if (message.includes('duplicate key value violates unique constraint')) {
|
||||
return '数据已存在,请勿重复添加';
|
||||
}
|
||||
if (message.includes('violates foreign key constraint')) {
|
||||
return '关联数据不存在或已被删除';
|
||||
}
|
||||
if (message.includes('violates not-null constraint')) {
|
||||
return '必填项不能为空';
|
||||
}
|
||||
if (message.includes('JSON object requested, multiple (or no) rows returned')) {
|
||||
return '数据查询错误:未找到记录或返回多条记录';
|
||||
}
|
||||
if (message.includes('Failed to fetch')) {
|
||||
return '网络请求失败,请检查网络连接';
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
94
lib/kr-utils.ts
Normal file
94
lib/kr-utils.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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;
|
||||
}
|
||||
6
lib/supabase.ts
Normal file
6
lib/supabase.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user