import dayjs from 'dayjs'; import { normalizeNullableStringId, normalizeStringId } from './shared'; type ProjectStatusCode = Api.Project.ProjectStatusCode; type ProjectStatusActionCode = Exclude; type StringIdResponse = string | number; export type ProjectLocalDateValue = string | number[] | null; export type LifecycleActionResponse = Partial> & { actionCode: ActionCode; }; export type ProjectExecutionResponse = Omit< Api.Project.ProjectExecution, | 'id' | 'projectId' | 'projectRequirementId' | 'ownerId' | 'availableActions' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate' | 'progressRate' | 'priority' | 'priorityName' > & { id: StringIdResponse; projectId: StringIdResponse; projectRequirementId?: StringIdResponse | null; ownerId: StringIdResponse; availableActions?: LifecycleActionResponse[] | null; plannedStartDate?: ProjectLocalDateValue; plannedEndDate?: ProjectLocalDateValue; actualStartDate?: ProjectLocalDateValue; actualEndDate?: ProjectLocalDateValue; progressRate?: number | null; priority?: string | number | null; priorityName?: string | null; }; export type MyExecutionResponse = Omit< Api.Project.MyExecutionItem, | 'id' | 'projectId' | 'projectRequirementId' | 'priority' | 'progressRate' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate' > & { id: StringIdResponse; projectId: StringIdResponse; projectRequirementId?: StringIdResponse | null; priority?: string | number | null; progressRate?: number | null; plannedStartDate?: ProjectLocalDateValue; plannedEndDate?: ProjectLocalDateValue; actualStartDate?: ProjectLocalDateValue; actualEndDate?: ProjectLocalDateValue; }; export type MyParticipatedProjectResponse = Omit & { id: StringIdResponse; }; export type MyOwnedProjectMemberResponse = Omit & { userId: StringIdResponse; }; export type MyOwnedProjectResponse = Omit & { id: StringIdResponse; members?: MyOwnedProjectMemberResponse[] | null; }; export type MyTaskResponse = Omit< Api.Project.MyTaskItem, | 'id' | 'projectId' | 'executionId' | 'priority' | 'plannedEndDate' | 'progressRate' | 'createTime' | 'parentTaskId' | 'availableActions' > & { id: StringIdResponse; projectId: StringIdResponse; executionId?: StringIdResponse | null; priority?: string | number | null; plannedEndDate?: ProjectLocalDateValue; progressRate?: number | string | null; createTime?: string | number | null; parentTaskId?: StringIdResponse | null; availableActions?: LifecycleActionResponse[] | null; }; export type TeamLoadDistributionItemResponse = Omit & { projectId?: StringIdResponse | null; }; export type TeamLoadMemberResponse = Omit & { userId: StringIdResponse; items?: TeamLoadDistributionItemResponse[] | null; }; export type TeamLoadResponse = { members?: TeamLoadMemberResponse[] | null; }; export type WorklogDistributionItemResponse = Omit & { projectId?: StringIdResponse | null; }; export type MyWorklogWeekResponse = Omit & { dailyHours?: number[] | null; distribution?: WorklogDistributionItemResponse[] | null; }; export type TeamWorklogWeekMemberResponse = Omit & { userId: StringIdResponse; items?: WorklogDistributionItemResponse[] | null; }; export type TeamWorklogWeekResponse = Omit & { members?: TeamWorklogWeekMemberResponse[] | null; }; export type ExecutionAssigneeResponse = Omit & { id: StringIdResponse; executionId: StringIdResponse; userId: StringIdResponse; }; export type ExecutionAssigneeLogResponse = Omit< Api.Project.ExecutionAssigneeLog, 'id' | 'executionId' | 'userId' | 'operatorUserId' > & { id: StringIdResponse; executionId: StringIdResponse; userId: StringIdResponse; operatorUserId: StringIdResponse; }; type TaskAssigneeRefResponse = Omit & { id: StringIdResponse; userId: StringIdResponse; }; /** * 后端 attachments 项的兼容形态:历史/当前响应字段名是 `id`,前端类型统一用 `fileId`。 * normalizeAttachments 负责把两者归一成 `fileId`。 */ type AttachmentItemResponse = Omit & { fileId?: StringIdResponse; id?: StringIdResponse; }; function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null { if (!list) { return null; } return list.map(item => { const rawId = item.fileId ?? item.id; return { ...item, fileId: rawId === null || rawId === undefined ? '' : String(rawId) }; }); } /** * 5.6 单独接口返的协办人字段(与 5.3 嵌入字段命名口径不一致:返 userNickname 而非 nickname)。 * 经 normalizeTaskAssignee 归一化后对外统一为 Api.Project.TaskAssigneeRef。 */ export type TaskAssigneeFromApiResponse = { id: StringIdResponse; taskId: StringIdResponse; userId: StringIdResponse; userNickname?: string | null; joinedAt?: string | null; }; export type TaskAssigneeLogResponse = Omit< Api.Project.TaskAssigneeLog, 'id' | 'taskId' | 'userId' | 'operatorUserId' > & { id: StringIdResponse; taskId: StringIdResponse; userId: StringIdResponse; operatorUserId: StringIdResponse; }; export type ProjectTaskResponse = Omit< Api.Project.ProjectTask, | 'id' | 'projectId' | 'executionId' | 'parentTaskId' | 'ownerId' | 'executionOwnerId' | 'parentTaskOwnerId' | 'availableActions' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate' | 'progressRate' | 'assignees' | 'attachments' | 'priority' | 'priorityName' > & { id: StringIdResponse; projectId: StringIdResponse; executionId: StringIdResponse; executionName?: string | null; executionStatusCode?: Api.Project.ProjectExecutionStatusCode | null; parentTaskId?: StringIdResponse | null; ownerId: StringIdResponse; executionOwnerId?: StringIdResponse | null; parentTaskOwnerId?: StringIdResponse | null; availableActions?: LifecycleActionResponse[] | null; plannedStartDate?: ProjectLocalDateValue; plannedEndDate?: ProjectLocalDateValue; actualStartDate?: ProjectLocalDateValue; actualEndDate?: ProjectLocalDateValue; progressRate?: number | null; assignees?: TaskAssigneeRefResponse[] | null; attachments?: AttachmentItemResponse[] | null; totalSpentHours?: number | null; priority?: string | number | null; priorityName?: string | null; }; export type TaskWorklogResponse = Omit< Api.Project.TaskWorklog, 'id' | 'taskId' | 'userId' | 'difficulty' | 'attachments' | 'startDate' | 'endDate' > & { id: StringIdResponse; taskId: StringIdResponse; userId: StringIdResponse; difficulty?: string | null; attachments?: AttachmentItemResponse[] | null; startDate?: ProjectLocalDateValue; endDate?: ProjectLocalDateValue; }; export interface ProjectMemberResponse { id: string | number; userId: string | number; userNickname: string; roleId: string | number; roleName: string; roleCode: string; managerFlag: boolean; status: 0 | 1; joinedTime: string; leftTime?: string | null; remark?: string | null; } const projectLifecycleActionNameMap: Record = { pause: '暂停项目', resume: '恢复项目', complete: '完成项目', cancel: '取消项目', reopen: '重新开启', archive: '归档项目' }; const projectLifecycleActionReasonRequiredMap: Record = { pause: true, resume: false, complete: true, cancel: true, reopen: true, archive: false }; const projectLifecycleActionMap: Record = { pending: ['cancel'], active: ['pause', 'complete', 'cancel'], paused: ['resume', 'cancel'], completed: ['reopen', 'archive'], cancelled: [], archived: [] }; export function getProjectLifecycleActions(statusCode: ProjectStatusCode): Api.Project.ProjectLifecycleAction[] { return projectLifecycleActionMap[statusCode].map(actionCode => ({ actionCode, actionName: projectLifecycleActionNameMap[actionCode], needReason: projectLifecycleActionReasonRequiredMap[actionCode] })); } export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefined) { if (value === null || value === undefined || value === '') { return null; } if (Array.isArray(value)) { const [year, month, day] = value; if (!year || !month || !day) { return null; } return [year, month, day].map(item => String(item).padStart(2, '0')).join('-'); } return String(value); } /** * 后端 LocalDateTime 统一序列化为毫秒时间戳(也可能是数字字符串/格式化字符串), * 归一为 'YYYY-MM-DD HH:mm:ss' 供展示与 dayjs 解析。 */ export function normalizeProjectDateTime(value: string | number | null | undefined): string { if (value === null || value === undefined || value === '') { return ''; } let parsed: dayjs.Dayjs; if (typeof value === 'number') { parsed = dayjs(value); } else if (/^\d+$/.test(value)) { // 字符串形态的毫秒时间戳:dayjs 无法直接解析,先转数值(时间值非 ID,安全整数范围内) parsed = dayjs(Number(value)); } else { parsed = dayjs(value); } return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm:ss') : ''; } export function normalizeLifecycleActions( actions: LifecycleActionResponse[] | null | undefined ): Api.Project.LifecycleAction[] { return (actions ?? []).map(action => ({ actionCode: action.actionCode, actionName: action.actionName ?? '', needReason: Boolean(action.needReason) })); } export function normalizeProjectMember(response: ProjectMemberResponse): Api.Project.ProjectMember { return { id: normalizeStringId(response.id), userId: normalizeStringId(response.userId), userNickname: response.userNickname || '', roleId: normalizeStringId(response.roleId), roleName: response.roleName || '', roleCode: response.roleCode || '', managerFlag: Boolean(response.managerFlag), status: response.status, joinedTime: response.joinedTime, leftTime: response.leftTime ?? null, remark: response.remark ?? null }; } function normalizePriority(value: string | number | null | undefined): string { if (value === null || value === undefined || value === '') { return '1'; } return String(value); } function normalizeProgressRate(value: number | string | null | undefined) { if (value === null || value === undefined || value === '') { return null; } const numeric = typeof value === 'number' ? value : Number(value ?? 0); return Number.isFinite(numeric) ? numeric : null; } export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution { return { ...response, id: normalizeStringId(response.id), projectId: normalizeStringId(response.projectId), projectRequirementId: normalizeNullableStringId(response.projectRequirementId), projectRequirementName: response.projectRequirementName ?? null, projectRequirementStatusCode: response.projectRequirementStatusCode ?? null, ownerId: normalizeStringId(response.ownerId), ownerNickname: response.ownerNickname ?? null, statusName: response.statusName ?? null, terminal: Boolean(response.terminal), allowEdit: Boolean(response.allowEdit), availableActions: normalizeLifecycleActions(response.availableActions), plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), actualStartDate: normalizeProjectLocalDate(response.actualStartDate), actualEndDate: normalizeProjectLocalDate(response.actualEndDate), progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, priority: normalizePriority(response.priority), priorityName: response.priorityName ?? null, executionDesc: response.executionDesc ?? null, lastStatusReason: response.lastStatusReason ?? null }; } export function normalizeMyExecution(response: MyExecutionResponse): Api.Project.MyExecutionItem { return { ...response, id: normalizeStringId(response.id), projectId: normalizeStringId(response.projectId), statusName: response.statusName ?? null, priority: normalizePriority(response.priority), progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), actualStartDate: normalizeProjectLocalDate(response.actualStartDate), actualEndDate: normalizeProjectLocalDate(response.actualEndDate), projectRequirementId: normalizeNullableStringId(response.projectRequirementId), projectRequirementName: response.projectRequirementName ?? null }; } export function normalizeMyParticipatedProject( response: MyParticipatedProjectResponse ): Api.Project.MyParticipatedProjectItem { return { ...response, id: normalizeStringId(response.id), code: response.code ?? null, statusName: response.statusName ?? null, myRole: response.myRole ?? null }; } export function normalizeMyOwnedProject(response: MyOwnedProjectResponse): Api.Project.MyOwnedProjectItem { return { ...response, id: normalizeStringId(response.id), code: response.code ?? null, myRole: response.myRole ?? null, plannedEndDate: response.plannedEndDate ?? null, members: (response.members ?? []).map(member => ({ ...member, userId: normalizeStringId(member.userId), userName: member.userName ?? null })) }; } export function normalizeMyTask(response: MyTaskResponse): Api.Project.MyTaskItem { return { ...response, id: normalizeStringId(response.id), projectId: normalizeStringId(response.projectId), executionId: normalizeNullableStringId(response.executionId), executionName: response.executionName ?? null, statusName: response.statusName ?? null, priority: normalizePriority(response.priority), plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), progressRate: normalizeProgressRate(response.progressRate) ?? 0, createTime: normalizeProjectDateTime(response.createTime), parentTaskId: normalizeNullableStringId(response.parentTaskId), terminal: Boolean(response.terminal), allowEdit: Boolean(response.allowEdit), availableActions: normalizeLifecycleActions(response.availableActions) }; } function normalizeWorklogDistributionItem( response: WorklogDistributionItemResponse | TeamLoadDistributionItemResponse ): { projectId: string | null; projectName: string | null; kind: 'project' | 'personal' | 'other' } { return { projectId: normalizeNullableStringId(response.projectId), projectName: response.projectName ?? null, kind: response.kind }; } export function normalizeTeamLoad(response: TeamLoadResponse): Api.Project.TeamLoadResult { return { members: (response.members ?? []).map(member => ({ userId: normalizeStringId(member.userId), userNickname: member.userNickname ?? '', items: (member.items ?? []).map(item => ({ ...normalizeWorklogDistributionItem(item), count: typeof item.count === 'number' ? item.count : 0 })), dueSoonCount: typeof member.dueSoonCount === 'number' ? member.dueSoonCount : 0, overdueCount: typeof member.overdueCount === 'number' ? member.overdueCount : 0 })) }; } export function normalizeMyWorklogWeek(response: MyWorklogWeekResponse): Api.Project.MyWorklogWeekResult { return { weekStart: response.weekStart ?? '', dailyHours: response.dailyHours ?? [0, 0, 0, 0, 0], distribution: (response.distribution ?? []).map(item => ({ ...normalizeWorklogDistributionItem(item), hours: typeof item.hours === 'number' ? item.hours : 0 })) }; } export function normalizeTeamWorklogWeek(response: TeamWorklogWeekResponse): Api.Project.TeamWorklogWeekResult { return { weekStart: response.weekStart ?? '', members: (response.members ?? []).map(member => ({ userId: normalizeStringId(member.userId), userNickname: member.userNickname ?? '', items: (member.items ?? []).map(item => ({ ...normalizeWorklogDistributionItem(item), hours: typeof item.hours === 'number' ? item.hours : 0 })) })) }; } export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee { return { ...response, id: normalizeStringId(response.id), executionId: normalizeStringId(response.executionId), userId: normalizeStringId(response.userId), userNickname: response.userNickname ?? null, joinedAt: response.joinedAt ?? null, removedAt: response.removedAt ?? null, removedReason: response.removedReason ?? null }; } export function normalizeExecutionAssigneeLog( response: ExecutionAssigneeLogResponse ): Api.Project.ExecutionAssigneeLog { return { ...response, id: normalizeStringId(response.id), executionId: normalizeStringId(response.executionId), userId: normalizeStringId(response.userId), operatorUserId: normalizeStringId(response.operatorUserId), userNicknameSnapshot: response.userNicknameSnapshot ?? null, operatorNicknameSnapshot: response.operatorNicknameSnapshot ?? null, reason: response.reason ?? null }; } export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project.ProjectTask { return { ...response, id: normalizeStringId(response.id), projectId: normalizeStringId(response.projectId), executionId: normalizeStringId(response.executionId), executionName: response.executionName ?? null, executionStatusCode: response.executionStatusCode ?? null, parentTaskId: normalizeNullableStringId(response.parentTaskId), projectRequirementId: normalizeNullableStringId(response.projectRequirementId), projectRequirementName: response.projectRequirementName ?? null, projectRequirementStatusCode: response.projectRequirementStatusCode ?? null, type: response.type ?? '', ownerId: normalizeStringId(response.ownerId), ownerNickname: response.ownerNickname ?? null, executionOwnerId: normalizeNullableStringId(response.executionOwnerId), parentTaskOwnerId: normalizeNullableStringId(response.parentTaskOwnerId), statusName: response.statusName ?? null, terminal: Boolean(response.terminal), allowEdit: Boolean(response.allowEdit), availableActions: normalizeLifecycleActions(response.availableActions), progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), actualStartDate: normalizeProjectLocalDate(response.actualStartDate), actualEndDate: normalizeProjectLocalDate(response.actualEndDate), priority: normalizePriority(response.priority), priorityName: response.priorityName ?? null, taskDesc: response.taskDesc ?? null, lastStatusReason: response.lastStatusReason ?? null, assignees: response.assignees?.map(item => ({ id: normalizeStringId(item.id), userId: normalizeStringId(item.userId), nickname: item.nickname ?? '' })) ?? null, attachments: normalizeAttachments(response.attachments), totalSpentHours: response.totalSpentHours ?? null }; } export function normalizeTaskWorklog(response: TaskWorklogResponse): Api.Project.TaskWorklog { return { ...response, id: normalizeStringId(response.id), taskId: normalizeStringId(response.taskId), userId: normalizeStringId(response.userId), userNickname: response.userNickname ?? null, workContent: response.workContent ?? null, attachments: normalizeAttachments(response.attachments), progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, // 后端 LocalDate 默认序列化为 [year, month, day] 数组,必须归一为 'YYYY-MM-DD' 字符串供 ElDatePicker 使用 startDate: normalizeProjectLocalDate(response.startDate) ?? '', endDate: normalizeProjectLocalDate(response.endDate) ?? '', // 历史记录或异常缺失时兜底为字典默认档位 "2" difficulty: response.difficulty ?? '2', difficultyName: response.difficultyName ?? null }; } export function normalizeTaskAssignee(response: TaskAssigneeFromApiResponse): Api.Project.TaskAssigneeRef { return { id: normalizeStringId(response.id), userId: normalizeStringId(response.userId), nickname: response.userNickname ?? '', joinedAt: response.joinedAt ?? null }; } export function normalizeTaskAssigneeLog(response: TaskAssigneeLogResponse): Api.Project.TaskAssigneeLog { return { ...response, id: normalizeStringId(response.id), taskId: normalizeStringId(response.taskId), userId: normalizeStringId(response.userId), operatorUserId: normalizeStringId(response.operatorUserId), userNicknameSnapshot: response.userNicknameSnapshot ?? null, operatorNicknameSnapshot: response.operatorNicknameSnapshot ?? null, reason: response.reason ?? null }; }