Files
ftb-project-management/apps/web/lib/requirement.ts
2026-07-02 09:30:50 +08:00

191 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Priority } from './derive';
export type RequirementStatus = 'pending_review' | 'adopted' | 'rejected' | 'planned' | 'developing' | 'testing' | 'released' | 'closed';
export type Effort = 'S' | 'M' | 'L' | 'XL';
export type SourceType = 'customer' | 'internal' | 'operation' | 'aftersale' | 'market' | 'competitor' | 'management';
export interface DictItem {
id: string;
name: string;
createdAt: string;
}
export interface SourceTarget {
id: string;
name: string;
sourceType: SourceType;
createdAt: string;
}
export function parseSourceTargetSelection(value?: string | null): string[] {
if (!value) return [];
return uniqueNonEmptyNames(value.split(/[、,]/));
}
export function formatSourceTargetSelection(names: string[]): string {
return uniqueNonEmptyNames(names).join('、');
}
export function filterSourceTargetsByKeyword(
targets: SourceTarget[],
sourceType: SourceType,
keyword: string,
): SourceTarget[] {
const query = keyword.trim().toLowerCase();
return targets.filter((target) => {
if (target.sourceType !== sourceType) return false;
if (!query) return true;
return target.name.toLowerCase().includes(query);
});
}
export interface RequirementProjectOption {
id: string;
name: string;
productId: string;
}
export interface RequirementVersionOption {
id: string;
name: string;
projectId: string;
}
export function filterRequirementProjectOptions(
projects: RequirementProjectOption[],
productId: string,
keyword: string,
): RequirementProjectOption[] {
if (!productId) return [];
const query = keyword.trim().toLowerCase();
return projects.filter((project) => {
if (project.productId !== productId) return false;
if (!query) return true;
return project.name.toLowerCase().includes(query);
});
}
export function filterRequirementVersionOptions(
versions: RequirementVersionOption[],
projectId: string,
keyword: string,
): RequirementVersionOption[] {
if (!projectId) return [];
const query = keyword.trim().toLowerCase();
return versions.filter((version) => {
if (version.projectId !== projectId) return false;
if (!query) return true;
return version.name.toLowerCase().includes(query);
});
}
function uniqueNonEmptyNames(names: string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const name of names) {
const trimmed = name.trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
out.push(trimmed);
}
return out;
}
export interface Requirement {
id: string;
code: string;
title: string;
description: string;
productId: string;
projectId: string;
versionId?: string;
sourceType: SourceType;
sourceTarget: string;
platforms: string[];
typeId: string;
status: RequirementStatus;
priority: Priority;
effort: Effort;
currentStage?: string;
productOwner?: string;
creator: string;
createdAt: string;
parentId?: string;
addedToVersionBy?: string;
reqType?: 'original' | 'change';
changeReason?: ChangeReason;
changeBy?: string;
}
export type ChangeReason = 'misunderstanding' | 'feedback_rework' | 'difficulty_exceeded' | 'upstream_delay';
export const CHANGE_REASON_LABEL: Record<ChangeReason, string> = {
misunderstanding: '需求理解偏差导致返工',
feedback_rework: '反馈导致返工',
difficulty_exceeded: '实现难度超预期',
upstream_delay: '上游交付延误',
};
export const SOURCE_TYPE_LABEL: Record<SourceType, string> = {
customer: '客户',
internal: '内部',
operation: '运营',
aftersale: '售后',
market: '市场',
competitor: '竞品',
management: '管理层',
};
export const SOURCE_TARGET_LABEL: Record<SourceType, string> = {
customer: '来源客户',
internal: '提出部门',
operation: '提出部门',
aftersale: '提出部门',
market: '来源渠道',
competitor: '竞品名称',
management: '提出人',
};
export const REQ_STATUS_LABEL: Record<RequirementStatus, string> = {
pending_review: '待评审',
adopted: '已采纳',
rejected: '已拒绝',
planned: '已规划',
developing: '开发中',
testing: '测试中',
released: '已上线',
closed: '已关闭',
};
export const REQ_STATUS_COLOR: Record<RequirementStatus, string> = {
pending_review: 'bg-zinc-100 text-zinc-700 border border-zinc-200',
adopted: 'bg-emerald-50 text-emerald-700 border border-emerald-200',
rejected: 'bg-red-50 text-red-600 border border-red-200',
planned: 'bg-blue-50 text-blue-700 border border-blue-200',
developing: 'bg-indigo-50 text-indigo-700 border border-indigo-200',
testing: 'bg-purple-50 text-purple-700 border border-purple-200',
released: 'bg-emerald-50 text-emerald-700 border border-emerald-200',
closed: 'bg-zinc-50 text-zinc-500 border border-zinc-200',
};
export const EFFORT_LABEL: Record<Effort, string> = {
S: 'S (1-2天)',
M: 'M (3-5天)',
L: 'L (5-10天)',
XL: 'XL (10+天)',
};
export const EFFORT_SHORT: Record<Effort, string> = {
S: 'S',
M: 'M',
L: 'L',
XL: 'XL',
};
export const EFFORT_COLOR: Record<Effort, string> = {
S: 'bg-emerald-50 text-emerald-700',
M: 'bg-blue-50 text-blue-700',
L: 'bg-orange-50 text-orange-700',
XL: 'bg-red-50 text-red-700',
};