feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
apps/web/lib/risk.ts
Normal file
95
apps/web/lib/risk.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { RoleProgress } from './derive';
|
||||
|
||||
export type RiskLevel = 'low' | 'medium' | 'high';
|
||||
export type DelayStatus = 'normal' | 'warning' | 'delayed';
|
||||
|
||||
const DELAY_WARNING_DAYS = 3;
|
||||
|
||||
export function calcOverallProgress(progress?: RoleProgress[]): number {
|
||||
if (!progress || progress.length === 0) return 0;
|
||||
const sum = progress.reduce((s, p) => s + p.percent, 0);
|
||||
return Math.round(sum / progress.length);
|
||||
}
|
||||
|
||||
export function calcDelayStatus(
|
||||
status: string,
|
||||
expectedReleaseDate?: string | null,
|
||||
): DelayStatus {
|
||||
if (status === 'released' || status === 'closed') return 'normal';
|
||||
if (!expectedReleaseDate) return 'normal';
|
||||
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
const deadline = new Date(expectedReleaseDate);
|
||||
deadline.setHours(0, 0, 0, 0);
|
||||
|
||||
const diffDays = (deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (diffDays < 0) return 'delayed';
|
||||
if (diffDays <= DELAY_WARNING_DAYS) return 'warning';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
export function calcRiskLevel(
|
||||
status: string,
|
||||
startDate?: string | null,
|
||||
expectedReleaseDate?: string | null,
|
||||
progress?: RoleProgress[],
|
||||
): RiskLevel {
|
||||
if (status === 'released' || status === 'closed') return 'low';
|
||||
if (status === 'paused') return 'medium';
|
||||
|
||||
const delayStatus = calcDelayStatus(status, expectedReleaseDate);
|
||||
if (delayStatus === 'delayed') return 'high';
|
||||
|
||||
if (!startDate || !expectedReleaseDate) return 'low';
|
||||
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
const start = new Date(startDate);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(expectedReleaseDate);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
|
||||
const totalDuration = end.getTime() - start.getTime();
|
||||
if (totalDuration <= 0) return 'low';
|
||||
|
||||
const elapsed = now.getTime() - start.getTime();
|
||||
const timeProgress = Math.min(100, Math.max(0, (elapsed / totalDuration) * 100));
|
||||
const actualProgress = calcOverallProgress(progress);
|
||||
const gap = timeProgress - actualProgress;
|
||||
|
||||
if (gap >= 40) return 'high';
|
||||
if (gap >= 20 || delayStatus === 'warning') return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
export const RISK_LABEL: Record<RiskLevel, string> = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
};
|
||||
|
||||
export const RISK_COLOR: Record<RiskLevel, string> = {
|
||||
low: 'text-emerald-600',
|
||||
medium: 'text-amber-600',
|
||||
high: 'text-red-600',
|
||||
};
|
||||
|
||||
export const RISK_DOT: Record<RiskLevel, string> = {
|
||||
low: 'bg-emerald-500',
|
||||
medium: 'bg-amber-500',
|
||||
high: 'bg-red-500',
|
||||
};
|
||||
|
||||
export const DELAY_LABEL: Record<DelayStatus, string> = {
|
||||
normal: '正常',
|
||||
warning: '即将延期',
|
||||
delayed: '已延期',
|
||||
};
|
||||
|
||||
export const DELAY_COLOR: Record<DelayStatus, string> = {
|
||||
normal: 'bg-emerald-50 text-emerald-700',
|
||||
warning: 'bg-amber-50 text-amber-700',
|
||||
delayed: 'bg-red-50 text-red-700',
|
||||
};
|
||||
Reference in New Issue
Block a user