feat(版本): 完善研发计划与预警已读
关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
115
apps/web/lib/version-form.ts
Normal file
115
apps/web/lib/version-form.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user