diff --git a/web/src/App.tsx b/web/src/App.tsx index 262899d1..6dff7349 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -49,6 +49,15 @@ import { import { mapApiMembersToProjectMembers } from './lib/projectMemberAdapter'; import { mergeWorkflowDocumentsFromServer } from './lib/deliverableSync'; import { resolveCurrentDocRole } from './lib/currentDocRole'; +import { holdsDocDuty } from './lib/docGovActionVisibility'; +import { + claimDocument, + submitForReview, + reviewDecision, + approverDecision, + mapLeaseToAstEditLease, + type DocGovRecord, +} from './api/docgov'; import { canAddBlockComment } from './lib/commentPolicy'; import { pollUntilPostTaskServerDoc, @@ -2934,8 +2943,18 @@ export default function App() { currentDocRole === 'author'; const isReviewPassShortcut = (text.includes('通过') || text.includes('同意')) && - ((docAST.lifecycleState === 'reviewing' && currentDocRole === 'reviewer') || - (docAST.lifecycleState === 'approving' && currentDocRole === 'approver')); + ((docAST.lifecycleState === 'reviewing' && + holdsDocDuty(currentUser.id, 'reviewer', { + authorId: docAST.authorId, + reviewerId: docAST.reviewerId, + approverId: docAST.approverId, + })) || + (docAST.lifecycleState === 'approving' && + holdsDocDuty(currentUser.id, 'approver', { + authorId: docAST.authorId, + reviewerId: docAST.reviewerId, + approverId: docAST.approverId, + }))); const sendActiveDoc = deliverables.find((d) => d.docId === activeDocId) || visibleDeliverables.find((d) => d.docId === activeDocId); @@ -3192,8 +3211,13 @@ export default function App() { } if (text.includes('通过') || text.includes('同意')) { - if (docAST.lifecycleState === 'reviewing' && currentDocRole === 'reviewer') { - handlePassReview(); + const duties = { + authorId: docAST.authorId, + reviewerId: docAST.reviewerId, + approverId: docAST.approverId, + }; + if (docAST.lifecycleState === 'reviewing' && holdsDocDuty(currentUser.id, 'reviewer', duties)) { + void handlePassReview(); setMessages((prev) => [ ...prev, { @@ -3208,8 +3232,8 @@ export default function App() { ]); return; } - if (docAST.lifecycleState === 'approving' && currentDocRole === 'approver') { - handleApprove(); + if (docAST.lifecycleState === 'approving' && holdsDocDuty(currentUser.id, 'approver', duties)) { + void handleApprove(); return; } } @@ -3328,80 +3352,189 @@ export default function App() { setDocAST({ ...docAST }); }; - // 法定生命周期流转 - const handleClaim = () => { - docAST.isClaimed = true; - setDocAST({ ...docAST }); + // 法定生命周期流转(真实 docgov HTTP) + const applyDocGovRecord = (doc: DocGovRecord) => { + const nextState = (doc.current_state || docAST.lifecycleState) as DocLifecycleState; + const nextClaimed = doc.author_claimed ?? docAST.isClaimed; + const lease = mapLeaseToAstEditLease(doc); + const nextAst: DocumentAST = { + ...docAST, + lifecycleState: nextState, + isClaimed: nextClaimed, + authorId: doc.author_id ?? docAST.authorId, + author: doc.author_name || docAST.author, + reviewerId: doc.reviewer_id ?? docAST.reviewerId, + reviewer: doc.reviewer_name || docAST.reviewer, + approverId: doc.approver_id ?? docAST.approverId, + approver: doc.approver_name || docAST.approver, + dutyPolicy: doc.duty_policy || docAST.dutyPolicy, + contentSha256: doc.content_sha256 || docAST.contentSha256, + editLease: lease ?? docAST.editLease, + }; + setDocAST(nextAst); setDeliverables((prev) => - prev.map((d) => (d.docId === activeDocId ? { ...d, isClaimed: true } : d)) + prev.map((d) => + d.docId === activeDocId || d.docId === doc.doc_id + ? { + ...d, + lifecycleState: nextState, + isClaimed: nextClaimed, + authorId: doc.author_id ?? d.authorId, + author: doc.author_name || d.author, + reviewerId: doc.reviewer_id ?? d.reviewerId, + reviewer: doc.reviewer_name || d.reviewer, + approverId: doc.approver_id ?? d.approverId, + approver: doc.approver_name || d.approver, + dutyPolicy: doc.duty_policy || d.dutyPolicy, + editLease: lease ?? d.editLease, + } + : d, + ), ); }; - const handleSubmitReview = () => { - docAST.lifecycleState = 'reviewing'; - setDocAST({ ...docAST }); - setDeliverables((prev) => - prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'reviewing' } : d)) - ); - }; - - const handlePassReview = () => { - docAST.lifecycleState = 'approving'; - setDocAST({ ...docAST }); - setDeliverables((prev) => - prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'approving' } : d)) - ); - }; - - const handleRejectReview = (reason: string) => { - docAST.lifecycleState = 'drafting'; - setDocAST({ ...docAST }); - setDeliverables((prev) => - prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'drafting' } : d)) - ); - + const pushDocGovError = (actionLabel: string, err: unknown) => { + const message = err instanceof Error ? err.message : String(err); setMessages((prev) => [ ...prev, { - id: 'rej-' + Date.now(), + id: 'docgov-err-' + Date.now(), sessionId: activeSessionId, role: 'system', - content: `⚠️ 李主管(审核负责人)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`, + content: `⚠️ ${actionLabel}失败:${message}`, timestamp: new Date().toTimeString().substring(0, 5), }, ]); }; - const handleApprove = () => { - docAST.lifecycleState = 'approved'; - setDocAST({ ...docAST }); - setDeliverables((prev) => - prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'approved' } : d)) - ); + const handleClaim = async () => { + try { + const doc = await claimDocument({ + doc_id: docAST.docId, + user_id: currentUser.id, + user_name: currentUser.name, + user_role: 'author', + }); + applyDocGovRecord(doc); + } catch (err) { + pushDocGovError('认领文档', err); + } + }; - setMessages((prev) => [ - ...prev, - { - id: 'appr-' + Date.now(), - sessionId: activeSessionId, - role: 'assistant', - authorName: '水木东方协同参谋', - intentTag: '🏆 终审批准生效', - content: `🎉 祝贺!王总监(批准人)已终审批准《${docAST.title}》!系统已自动对该受控交付物进行微粒度章节切片与向量化,成功注入全局标杆案例经验库!`, - timestamp: new Date().toTimeString().substring(0, 5), - recommendedCases: [ - { - docId: docAST.docId, - taskKey: docAST.taskKey, - artifactTitle: docAST.title, - sectionTitle: '终审已生效标杆案例', - similarityScore: 1.0, - keyRegulations: ['GB 9706.1-2020', 'YY 0569-2011', 'IEC 60601-1-2'], - summarySnippet: '本交付物已通过编制、审核、批准法定三级签署闭环,作为往期标杆经验向全平台开放自推荐。', - }, - ], - }, - ]); + const handleSubmitReview = async () => { + try { + const doc = await submitForReview({ + doc_id: docAST.docId, + user_id: currentUser.id, + user_name: currentUser.name, + }); + applyDocGovRecord(doc); + } catch (err) { + pushDocGovError('提交送审', err); + } + }; + + const handlePassReview = async () => { + try { + const doc = await reviewDecision({ + doc_id: docAST.docId, + user_id: currentUser.id, + user_name: currentUser.name, + action: 'approve', + }); + applyDocGovRecord(doc); + } catch (err) { + pushDocGovError('审核通过', err); + } + }; + + const handleRejectReview = async (reason: string) => { + try { + const doc = await reviewDecision({ + doc_id: docAST.docId, + user_id: currentUser.id, + user_name: currentUser.name, + action: 'reject', + reason, + }); + applyDocGovRecord(doc); + setMessages((prev) => [ + ...prev, + { + id: 'rej-' + Date.now(), + sessionId: activeSessionId, + role: 'system', + content: `⚠️ ${currentUser.name}(审核)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } catch (err) { + pushDocGovError('审核驳回', err); + } + }; + + const handleApprove = async () => { + try { + const doc = await approverDecision({ + doc_id: docAST.docId, + user_id: currentUser.id, + user_name: currentUser.name, + action: 'approve', + }); + applyDocGovRecord(doc); + setMessages((prev) => [ + ...prev, + { + id: 'appr-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '🏆 终审批准生效', + content: `🎉 祝贺!${currentUser.name}(批准人)已终审批准《${docAST.title}》!系统已自动对该受控交付物进行微粒度章节切片与向量化,成功注入全局标杆案例经验库!`, + timestamp: new Date().toTimeString().substring(0, 5), + recommendedCases: [ + { + docId: docAST.docId, + taskKey: docAST.taskKey, + artifactTitle: docAST.title, + sectionTitle: '终审已生效标杆案例', + similarityScore: 1.0, + keyRegulations: ['GB 9706.1-2020', 'YY 0569-2011', 'IEC 60601-1-2'], + summarySnippet: + '本交付物已通过编制、审核、批准法定三级签署闭环,作为往期标杆经验向全平台开放自推荐。', + }, + ], + }, + ]); + } catch (err) { + pushDocGovError('终审批准', err); + } + }; + + const handleRejectApprove = async (reason: string) => { + try { + const doc = await approverDecision({ + doc_id: docAST.docId, + user_id: currentUser.id, + user_name: currentUser.name, + action: 'reject', + reason, + rollback_to: 'drafting', + }); + applyDocGovRecord(doc); + setMessages((prev) => [ + ...prev, + { + id: 'rej-appr-' + Date.now(), + sessionId: activeSessionId, + role: 'system', + content: `⚠️ ${currentUser.name}(批准人)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } catch (err) { + pushDocGovError('批准驳回', err); + } }; // 导出 DOCX (Word 渲染器) @@ -4206,7 +4339,7 @@ export default function App() { onPassReview={handlePassReview} onRejectReview={handleRejectReview} onApprove={handleApprove} - onRejectApprove={handleRejectReview} + onRejectApprove={handleRejectApprove} onShareToProject={handleShareToProject} onRevokeShare={handleRevokeShare} />