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

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

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

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

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

116 lines
3.8 KiB
TypeScript

import { isChinaWorkday } from './china-workday-calendar';
import { formatLocalDate } from './format';
export type VersionDevelopmentType = 'agile' | 'standard' | 'waterfall' | 'hotfix';
export interface VersionDevelopmentStageEstimate {
key: 'product_design' | 'development' | 'testing';
label: '产品设计' | '开发' | '测试';
workDays: number;
}
export interface VersionDevelopmentTypeOption {
key: VersionDevelopmentType;
label: string;
description: string;
stages: VersionDevelopmentStageEstimate[];
totalWorkDays: number;
}
export interface NewVersionFormState {
productId: string;
projectId: string;
versionNumber: string;
developmentType: VersionDevelopmentType | '';
expectedReleaseDate: string;
submitting: boolean;
}
export const VERSION_DEVELOPMENT_TYPE_OPTIONS: VersionDevelopmentTypeOption[] = [
makeDevelopmentTypeOption('agile', '敏捷迭代', '适合常规双周迭代', [
{ key: 'product_design', label: '产品设计', workDays: 2 },
{ key: 'development', label: '开发', workDays: 8 },
{ key: 'testing', label: '测试', workDays: 3 },
]),
makeDevelopmentTypeOption('standard', '常规项目', '适合需求较完整的普通版本', [
{ key: 'product_design', label: '产品设计', workDays: 3 },
{ key: 'development', label: '开发', workDays: 12 },
{ key: 'testing', label: '测试', workDays: 4 },
]),
makeDevelopmentTypeOption('waterfall', '瀑布项目', '适合方案先行的大范围交付', [
{ key: 'product_design', label: '产品设计', workDays: 5 },
{ key: 'development', label: '开发', workDays: 20 },
{ key: 'testing', label: '测试', workDays: 7 },
]),
makeDevelopmentTypeOption('hotfix', '紧急修复', '适合线上缺陷或很小范围改动', [
{ key: 'product_design', label: '产品设计', workDays: 0 },
{ key: 'development', label: '开发', workDays: 2 },
{ key: 'testing', label: '测试', workDays: 1 },
]),
];
export function canSubmitNewVersionForm(state: NewVersionFormState): boolean {
return Boolean(
state.productId &&
state.projectId &&
state.versionNumber &&
state.developmentType &&
state.expectedReleaseDate &&
!state.submitting,
);
}
export function getVersionDevelopmentTypeOption(type: VersionDevelopmentType): VersionDevelopmentTypeOption | undefined {
return VERSION_DEVELOPMENT_TYPE_OPTIONS.find((option) => option.key === type);
}
export function getRecommendedExpectedReleaseDate(
type: VersionDevelopmentType,
baseDate: Date = new Date(),
options: { productDesignCompleted?: boolean } = {},
): string {
const developmentType = getVersionDevelopmentTypeOption(type);
if (!developmentType) return '';
const totalWorkDays = getRecommendedWorkDays(developmentType, options);
return addChinaWorkDays(baseDate, totalWorkDays);
}
export function getRecommendedWorkDays(
option: VersionDevelopmentTypeOption,
options: { productDesignCompleted?: boolean } = {},
): number {
return option.stages.reduce((sum, stage) => {
if (stage.key === 'product_design' && options.productDesignCompleted) return sum;
return sum + stage.workDays;
}, 0);
}
function makeDevelopmentTypeOption(
key: VersionDevelopmentType,
label: string,
description: string,
stages: VersionDevelopmentStageEstimate[],
): VersionDevelopmentTypeOption {
return {
key,
label,
description,
stages,
totalWorkDays: stages.reduce((sum, stage) => sum + stage.workDays, 0),
};
}
function addChinaWorkDays(baseDate: Date, workDays: number): string {
const cursor = new Date(baseDate);
cursor.setHours(0, 0, 0, 0);
let remaining = Math.max(1, Math.round(workDays));
while (remaining > 0) {
if (isChinaWorkday(cursor)) remaining -= 1;
if (remaining <= 0) break;
cursor.setDate(cursor.getDate() + 1);
}
return formatLocalDate(cursor);
}