Files
ftb-project-management/apps/web/lib/version-plan-workflow.ts
Script Generator 3d3d56697a feat(版本): 完善研发计划与预警已读
关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-06-30 16:48:58 +08:00

122 lines
4.5 KiB
TypeScript

import { getRequirementCoverageSummary } from './version-plan';
import type { ProductPlanKind, ProductPlanReviewFailureType, ProductPlanReviewResult, VersionPlan } from './version-plan';
export interface PlanResultPayload {
type?: VersionPlan['type'];
productPlanKind?: ProductPlanKind;
resultType?: 'link' | 'file';
resultTitle?: string;
resultUrl?: string;
resultFileName?: string;
resultFileData?: string;
prototypeReviewConfirmed?: boolean;
reviewResult?: ProductPlanReviewResult;
reviewFailureTypes?: ProductPlanReviewFailureType[];
reviewFailureReason?: string;
}
export interface PlanCompletionState {
checklistTotal: number;
checklistCompleted: number;
requirementTotal: number;
requirementCompleted: number;
hasResult: boolean;
canSubmitResult: boolean;
canComplete: boolean;
missingReasons: string[];
}
export function hasPlanResult(plan: PlanResultPayload): boolean {
if (plan.productPlanKind === 'review') {
if (plan.reviewResult === 'passed') return true;
if (plan.reviewResult === 'failed') {
return Boolean(plan.reviewFailureTypes?.length && plan.reviewFailureReason?.trim());
}
return false;
}
if (plan.productPlanKind === 'design') {
return Boolean(
plan.resultType === 'link'
&& plan.resultTitle?.trim()
&& plan.resultUrl?.trim()
&& plan.prototypeReviewConfirmed,
);
}
const hasTitle = Boolean(plan.resultTitle?.trim());
if (!hasTitle || !plan.resultType) return false;
if (plan.resultType === 'link') return Boolean(plan.resultUrl?.trim());
return Boolean(plan.resultFileData || plan.resultFileName);
}
function requiresRequirementCoverage(plan: VersionPlan): boolean {
return plan.type === 'product' || plan.type === 'ui';
}
function requiresChecklist(plan: VersionPlan): boolean {
return plan.type === 'research';
}
function getProductPlanKind(plan: VersionPlan): ProductPlanKind | undefined {
if (plan.type !== 'product') return undefined;
return plan.productPlanKind ?? 'design';
}
export function getPlanCompletionState(plan: VersionPlan): PlanCompletionState {
const tasks = plan.tasks ?? [];
const checklistTotal = tasks.length;
const checklistCompleted = tasks.filter((task) => task.status === 'completed').length;
const requirementSummary = getRequirementCoverageSummary(plan);
const requirementTotal = requirementSummary.total;
const requirementCompleted = requirementSummary.completed;
const missingReasons: string[] = [];
if (requiresChecklist(plan) && checklistTotal === 0) missingReasons.push('缺少调研方向');
if (requiresChecklist(plan) && checklistTotal > 0 && checklistCompleted < checklistTotal) missingReasons.push('调研方向未全部完成');
if (requiresRequirementCoverage(plan) && requirementTotal > 0 && requirementCompleted < requirementTotal) {
missingReasons.push('关联需求未全部覆盖');
}
const canSubmitResult = missingReasons.length === 0;
const productPlanKind = getProductPlanKind(plan);
const resultPlan = productPlanKind ? { ...plan, productPlanKind } : plan;
const hasResult = hasPlanResult(resultPlan);
if (canSubmitResult && !hasResult) {
if (productPlanKind === 'design') {
if (plan.resultType === 'file') missingReasons.push('产品设计方案只支持原型链接');
if (plan.resultType === 'link' && plan.resultUrl?.trim() && !plan.prototypeReviewConfirmed) {
missingReasons.push('请先确认方案评审已通过');
} else {
missingReasons.push('尚未提交原型链接');
}
} else if (productPlanKind === 'review') {
if (!plan.reviewResult) missingReasons.push('请选择评审结论');
if (plan.reviewResult === 'failed' && !plan.reviewFailureTypes?.length) missingReasons.push('请选择评审不通过类型');
if (plan.reviewResult === 'failed' && !plan.reviewFailureReason?.trim()) missingReasons.push('请填写评审不通过原因');
} else {
missingReasons.push('尚未提交成果');
}
}
return {
checklistTotal,
checklistCompleted,
requirementTotal,
requirementCompleted,
hasResult,
canSubmitResult,
canComplete: canSubmitResult && hasResult,
missingReasons,
};
}
export function canTogglePlanChecklist(plan: VersionPlan, now: Date = new Date()): boolean {
return plan.status === 'in_progress' || (plan.status === 'pending' && Boolean(plan.startTime) && new Date(plan.startTime) <= now);
}
export function canEditPlanRequirementCoverage(plan: VersionPlan, now: Date = new Date()): boolean {
return canTogglePlanChecklist(plan, now);
}