// 任务模块: 跨项目拷贝 (T-CP-*) // 拷贝而非移动: 新建记录 + 深拷贝子任务, 源任务零改动. import { prisma } from '../db.js' /** * 拷贝一条任务(含子任务)到目标项目。 * * 规则: * - 全字段复制: 标题/描述/状态/列/扩展卡片/标签 (T-CP-02) * - 子任务一并复制, 且归属目标项目 (T-CP-03, T-CP-08) * - creatorId = 操作者 (T-CP-04) * - 负责人 / 任务成员只保留"属于目标项目成员"的部分, 其余丢弃并回报 (T-CP-07, T-EX-08) * - 状态与列沿用源值, 不强行纠正 (T-EX-14) * * @returns {{ task, dropped: { assignee: null|{id,name}, members: {id,name}[] } }} */ export async function copyTask({ source, targetProject, targetColumns, operatorId, targetMemberIds }) { const memberSet = new Set(targetMemberIds) // 列映射: 目标项目里找同名列, 找不到就用第一列 (保证 columnId 合法) const pickColumn = (sourceColumnId) => { const srcCol = source.__columns?.find((c) => c.id === sourceColumnId) if (srcCol) { const same = targetColumns.find((c) => c.name === srcCol.name) if (same) return same.id } return targetColumns[0]?.id } // 人员过滤 const keepAssignee = source.assigneeId && memberSet.has(source.assigneeId) ? source.assigneeId : null const droppedMembers = [] const keptMemberIds = [] for (const m of source.members || []) { if (memberSet.has(m.userId)) keptMemberIds.push(m.userId) else droppedMembers.push(m.user) } const droppedAssignee = source.assigneeId && !memberSet.has(source.assigneeId) ? source.assignee : null const baseData = (extra) => ({ projectId: targetProject.id, title: source.title, description: source.description || '', status: source.status, columnId: extra.columnId, parentId: extra.parentId ?? null, assigneeId: extra.parentId ? (source.assigneeId && memberSet.has(source.assigneeId) ? source.assigneeId : null) : keepAssignee, creatorId: operatorId, extras: source.extras ?? [], doneAt: source.status === 'done' ? (source.doneAt || new Date()) : null, sortOrder: source.sortOrder ?? 0, tags: { create: (source.tags || []).map((t) => ({ name: t.name })), }, }) const newTask = await prisma.task.create({ data: { ...baseData({ columnId: pickColumn(source.columnId) }), members: { create: keptMemberIds.map((uid) => ({ userId: uid })), }, }, }) // 子任务: 同法复制, parentId 指向新父任务 for (const child of source.children || []) { const childMembers = (child.members || []) .filter((m) => memberSet.has(m.userId)) .map((m) => m.userId) await prisma.task.create({ data: { ...baseData({ columnId: pickColumn(child.columnId), parentId: newTask.id, }), members: { create: childMembers.map((uid) => ({ userId: uid })) }, }, }) } return { task: newTask, dropped: { assignee: droppedAssignee ? { id: droppedAssignee.id, name: droppedAssignee.name } : null, members: droppedMembers.map((u) => ({ id: u.id, name: u.name })), }, } }