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 interface VersionEditFormState { versionNumber: string; 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 canSubmitVersionEditForm(state: VersionEditFormState): boolean { return Boolean( state.versionNumber.trim() && state.expectedReleaseDate && !state.submitting, ); } export function getEditableVersionNumber(projectName: string, versionName: string): string { const exactPrefix = `${projectName}V`; if (projectName && versionName.toLowerCase().startsWith(exactPrefix.toLowerCase())) { return versionName.slice(exactPrefix.length); } const match = versionName.match(/V([\d.]+)$/i); return match?.[1] ?? versionName; } export function buildVersionName(projectName: string, versionNumber: string): string { return `${projectName}V${versionNumber.trim()}`; } 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); }