fix: force dynamic for meeting page

This commit is contained in:
李超14pro
2026-01-25 21:55:29 +08:00
commit e3e5e89df6
112 changed files with 19073 additions and 0 deletions

33
lib/constants.ts Normal file
View 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
View 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
View 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
View 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
View 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));
}