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 linked = plan.linkedRequirementIds ?? []; const completed = new Set(plan.completedRequirementIds ?? []); const requirementTotal = linked.length; const requirementCompleted = linked.filter((id) => completed.has(id)).length; 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); }