// 文档职责草稿的唯一存储。三个视图只投影这份数据;单元格值由三权反推。 export type CellDuty = 'none' | 'author' | 'reviewer' | 'approver' interface DutyRoles { author: string reviewers: string[] approver: string } export interface DutyDocDraft extends DutyRoles { docId: string title: string code: string typeKey: string stage: string dutyVersion: number activeRound: number legacySectionOverrides: number dirty: boolean } export interface DutyMember { owner: string role: string valid: boolean canAuthor: boolean canReview: boolean canApprove: boolean } interface DutySaveItem { docId: string author: string reviewers: string[] approver: string expectedDutyVersion: number confirmInvalidation: boolean } export function cellDuty(roles: DutyRoles, owner: string): CellDuty { if (roles.author === owner) return 'author' if (roles.approver === owner) return 'approver' if (roles.reviewers.includes(owner)) return 'reviewer' return 'none' } export function applyCell(roles: DutyRoles, owner: string, duty: CellDuty): { roles: DutyRoles replacedAuthor: string replacedApprover: string } { const replacedAuthor = duty === 'author' && roles.author !== '' && roles.author !== owner ? roles.author : '' const replacedApprover = duty === 'approver' && roles.approver !== '' && roles.approver !== owner ? roles.approver : '' let author = roles.author === owner ? '' : roles.author let approver = roles.approver === owner ? '' : roles.approver let reviewers = roles.reviewers.filter(person => person !== owner) switch (duty) { case 'author': author = owner break case 'approver': approver = owner break case 'reviewer': reviewers = [...reviewers, owner] break case 'none': break default: { const exhaustive: never = duty throw new Error(exhaustive) } } return { roles: { author, reviewers, approver }, replacedAuthor, replacedApprover } } export function replacementNote(roles: DutyRoles, owner: string, duty: CellDuty): string { if (duty === 'author' && roles.author !== '' && roles.author !== owner) return `将替换${roles.author}` if (duty === 'approver' && roles.approver !== '' && roles.approver !== owner) return `将替换${roles.approver}` return '' } function dutyConflicts(roles: DutyRoles): number { const seen = new Map() const mark = (owner: string) => { if (!owner) return seen.set(owner, (seen.get(owner) ?? 0) + 1) } mark(roles.author) mark(roles.approver) for (const reviewer of roles.reviewers) mark(reviewer) let conflicts = 0 for (const count of seen.values()) if (count > 1) conflicts += 1 return conflicts } export function completeness(roles: DutyRoles): { authors: number; reviewers: number; approvers: number; conflicts: number } { return { authors: roles.author ? 1 : 0, reviewers: roles.reviewers.length, approvers: roles.approver ? 1 : 0, conflicts: dutyConflicts(roles), } } export function saveIssues(roles: DutyRoles): string[] { const issues: string[] = [] if (!roles.author) issues.push('需指定编制人') if (roles.reviewers.length === 0) issues.push('审核至少一人') if (!roles.approver) issues.push('需指定批准人') if (dutyConflicts(roles) > 0) issues.push('一人只能担一个职责') return issues } function rolesFromCells(docId: string, cells: { docId: string; owner: string; duty: string }[]): DutyRoles { const roles: DutyRoles = { author: '', reviewers: [], approver: '' } for (const cell of cells) { if (cell.docId !== docId) continue if (cell.duty !== 'none' && cell.duty !== 'author' && cell.duty !== 'reviewer' && cell.duty !== 'approver') continue const next = applyCell(roles, cell.owner, cell.duty) roles.author = next.roles.author roles.reviewers = next.roles.reviewers roles.approver = next.roles.approver } return roles } export function absorbServerDocs( current: DutyDocDraft[], docs: { docId: string; title: string; code?: string; typeKey: string; stage?: string dutyVersion: number; activeRound?: number; legacySectionOverrides?: number }[], cells: { docId: string; owner: string; duty: string }[], replaceDirty = false, ): DutyDocDraft[] { const byId = new Map(current.map(doc => [doc.docId, doc])) const pageIds = new Set(docs.map(doc => doc.docId)) const next: DutyDocDraft[] = [] for (const doc of docs) { const previous = byId.get(doc.docId) const serverRoles = rolesFromCells(doc.docId, cells) const keepDraft = !replaceDirty && previous?.dirty === true next.push({ docId: doc.docId, title: doc.title, code: doc.code ?? doc.docId, typeKey: doc.typeKey, stage: doc.stage ?? '', dutyVersion: keepDraft ? previous.dutyVersion : doc.dutyVersion, activeRound: doc.activeRound ?? 0, legacySectionOverrides: doc.legacySectionOverrides ?? 0, dirty: keepDraft, author: keepDraft ? previous.author : serverRoles.author, reviewers: keepDraft ? previous.reviewers : serverRoles.reviewers, approver: keepDraft ? previous.approver : serverRoles.approver, }) } for (const doc of current) { if (!replaceDirty && doc.dirty && !pageIds.has(doc.docId)) next.push(doc) } return next } export function editCell(docs: DutyDocDraft[], docId: string, owner: string, duty: CellDuty): DutyDocDraft[] { return docs.map(doc => { if (doc.docId !== docId) return doc const next = applyCell(doc, owner, duty) return { ...doc, ...next.roles, dirty: true } }) } export function dirtyDocs(docs: DutyDocDraft[]): DutyDocDraft[] { return docs.filter(doc => doc.dirty) } export function savePayload(docs: DutyDocDraft[], confirmInvalidation: boolean): { docs: DutySaveItem[]; issues: string[] } { const dirty = dirtyDocs(docs) const issues: string[] = [] if (dirty.length === 0) issues.push('没有未保存的职责') if (dirty.length > 20) issues.push('一次保存最多 20 份文档') for (const doc of dirty) { for (const issue of saveIssues(doc)) issues.push(`${doc.title}:${issue}`) } return { docs: dirty.slice(0, 20).map(doc => ({ docId: doc.docId, author: doc.author, reviewers: doc.reviewers, approver: doc.approver, expectedDutyVersion: doc.dutyVersion, confirmInvalidation: confirmInvalidation && doc.activeRound > 0, })), issues, } }