299 lines
9.5 KiB
TypeScript
299 lines
9.5 KiB
TypeScript
import type { Dispatch, SetStateAction } from 'react';
|
|
import {
|
|
approverDecision,
|
|
claimDocument,
|
|
mapLeaseToAstEditLease,
|
|
reviewDecision,
|
|
submitForReview,
|
|
type DocGovRecord,
|
|
} from '../api/docgov';
|
|
import { requestReopen } from '../api/projectTrace';
|
|
import { fetchProjectSettings } from '../api/projectSettings';
|
|
import type { DocumentAST } from '../types/block';
|
|
import type { DocLifecycleState, UserProfile } from '../types/docgov';
|
|
import type { ChatMessage, DeliverableItem } from '../types/workbench';
|
|
|
|
export interface DocumentLifecycleInput {
|
|
docAST: DocumentAST;
|
|
setDocAST: (next: DocumentAST) => void;
|
|
activeDocId: string;
|
|
currentUser: UserProfile;
|
|
activeSessionId: string;
|
|
projectId?: number;
|
|
workflowInstanceId?: number;
|
|
setDeliverables: Dispatch<SetStateAction<DeliverableItem[]>>;
|
|
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
|
|
}
|
|
|
|
export interface DocumentLifecycleActions {
|
|
handleClaim: () => Promise<void>;
|
|
handleSubmitReview: () => Promise<void>;
|
|
handlePassReview: () => Promise<void>;
|
|
handleRejectReview: (reason: string) => Promise<void>;
|
|
handleApprove: () => Promise<void>;
|
|
handleRejectApprove: (reason: string) => Promise<void>;
|
|
handleRequestReopen: () => Promise<void>;
|
|
}
|
|
|
|
export function useDocumentLifecycleActions(input: DocumentLifecycleInput): DocumentLifecycleActions {
|
|
const {
|
|
docAST,
|
|
setDocAST,
|
|
activeDocId,
|
|
currentUser,
|
|
activeSessionId,
|
|
projectId,
|
|
workflowInstanceId,
|
|
setDeliverables,
|
|
setMessages,
|
|
} = input;
|
|
|
|
const applyDocGovRecord = (doc: DocGovRecord) => {
|
|
// Prefer API current_state; never silently keep reviewing after reject if server omitted field.
|
|
const apiState = (doc.current_state || '').trim();
|
|
const nextState = (apiState || docAST.lifecycleState) as DocLifecycleState;
|
|
const nextClaimed = doc.author_claimed ?? docAST.isClaimed;
|
|
const lease = mapLeaseToAstEditLease(doc);
|
|
const nextAst: DocumentAST = {
|
|
...docAST,
|
|
lifecycleState: nextState,
|
|
isClaimed: nextClaimed,
|
|
authorClaimedAt: doc.author_claimed_at || docAST.authorClaimedAt,
|
|
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.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 pushDocGovError = (actionLabel: string, err: unknown) => {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
id: 'docgov-err-' + Date.now(),
|
|
sessionId: activeSessionId,
|
|
role: 'system',
|
|
content: `⚠️ ${actionLabel}失败:${message}`,
|
|
timestamp: new Date().toTimeString().substring(0, 5),
|
|
},
|
|
]);
|
|
};
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
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);
|
|
throw 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);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const handleRequestReopen = async () => {
|
|
if (!projectId || projectId <= 0) {
|
|
pushDocGovError('申请再修订', new Error('缺少项目上下文'));
|
|
return;
|
|
}
|
|
const reason = window.prompt('请输入再修订原因(将通知原批准人审批)');
|
|
if (!reason || !reason.trim()) return;
|
|
try {
|
|
let instanceId = workflowInstanceId || 0;
|
|
let stageKey = docAST.version || '';
|
|
try {
|
|
const settings = await fetchProjectSettings(projectId);
|
|
if (settings?.live_instance?.id) {
|
|
instanceId = settings.live_instance.id;
|
|
}
|
|
if (settings?.active_stage_key) {
|
|
stageKey = settings.active_stage_key;
|
|
}
|
|
} catch {
|
|
/* keep fallbacks */
|
|
}
|
|
if (!stageKey) stageKey = 'V0.5';
|
|
const stored = await requestReopen(projectId, {
|
|
instance_id: instanceId,
|
|
stage_key: stageKey,
|
|
doc_task_keys: [docAST.taskKey],
|
|
reason: reason.trim(),
|
|
});
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
id: 'reopen-' + Date.now(),
|
|
sessionId: activeSessionId,
|
|
role: 'system',
|
|
content:
|
|
stored.status === 'approved'
|
|
? `《${docAST.title}》再修订申请已自动批准,文档已回编制期。`
|
|
: `已提交《${docAST.title}》再修订申请(#${stored.id}),等待原批准人审批。`,
|
|
timestamp: new Date().toTimeString().substring(0, 5),
|
|
},
|
|
]);
|
|
if (stored.status === 'approved') {
|
|
setDocAST({ ...docAST, lifecycleState: 'drafting' as DocLifecycleState, isClaimed: docAST.isClaimed });
|
|
}
|
|
} catch (err) {
|
|
pushDocGovError('申请再修订', err);
|
|
}
|
|
};
|
|
|
|
return {
|
|
handleClaim,
|
|
handleSubmitReview,
|
|
handlePassReview,
|
|
handleRejectReview,
|
|
handleApprove,
|
|
handleRejectApprove,
|
|
handleRequestReopen,
|
|
};
|
|
}
|