feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面
关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
这个提交包含在:
@@ -28,6 +28,7 @@ import {
|
||||
removeWorkflowActionDraft,
|
||||
resolveWorkflowTaskReferenceSnapshot,
|
||||
updateWorkflowActionDraftStatus,
|
||||
type WorkflowDraftAction,
|
||||
type WorkflowDraftQueueItem,
|
||||
} from "../mobile/workflowActionQueue";
|
||||
|
||||
@@ -107,6 +108,7 @@ export function MobileApprovalsPage() {
|
||||
const [comment, setComment] = useState("");
|
||||
const [transferToApproverId, setTransferToApproverId] = useState("");
|
||||
const [transferReason, setTransferReason] = useState("");
|
||||
const [offlineDraftSubmitting, setOfflineDraftSubmitting] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState<boolean>(typeof navigator === "undefined" ? true : navigator.onLine);
|
||||
const autoSyncRequestedRef = useRef(false);
|
||||
|
||||
@@ -192,14 +194,63 @@ export function MobileApprovalsPage() {
|
||||
queryFn: () => fetchWorkflowApprovers({ page: 1, pageSize: 100 }),
|
||||
});
|
||||
|
||||
async function refreshDraftQueueCache() {
|
||||
if (!ownerEmployeeId) {
|
||||
return [];
|
||||
}
|
||||
const drafts = await listWorkflowActionDrafts(ownerEmployeeId);
|
||||
queryClient.setQueryData(["mobile", "workflow", "draft-queue", ownerEmployeeId], drafts);
|
||||
return drafts;
|
||||
}
|
||||
|
||||
const currentQueueItems = queueQuery.data ?? [];
|
||||
const retryableQueueItems = useMemo(
|
||||
() => currentQueueItems.filter((item) => item.status !== "conflicted"),
|
||||
[currentQueueItems],
|
||||
);
|
||||
async function queueOfflineDraft(action: WorkflowDraftAction) {
|
||||
try {
|
||||
if (!selectedTaskId) {
|
||||
throw new Error("请选择任务");
|
||||
}
|
||||
if (!selectedTask) {
|
||||
throw new Error("当前待办尚未缓存,请联网加载后再操作");
|
||||
}
|
||||
if (!selectedTask.allowedActions.includes(action)) {
|
||||
throw new Error("当前待办不允许该动作");
|
||||
}
|
||||
if (action === "reject" && !comment.trim()) {
|
||||
throw new Error("驳回必须填写意见");
|
||||
}
|
||||
|
||||
setOfflineDraftSubmitting(true);
|
||||
await enqueueWorkflowActionDraft({
|
||||
taskId: selectedTaskId,
|
||||
action,
|
||||
comment: action === "reject" ? comment.trim() : null,
|
||||
ownerEmployeeId,
|
||||
detail: selectedTask,
|
||||
});
|
||||
await refreshDraftQueueCache();
|
||||
setComment("");
|
||||
setError("");
|
||||
setMessage(action === "approve" ? "当前离线,通过草稿已加入待同步队列。" : "当前离线,驳回草稿已加入待同步队列。");
|
||||
toast.success(action === "approve" ? "已加入通过草稿队列" : "已加入驳回草稿队列");
|
||||
} catch (actionError) {
|
||||
const nextError = resolveErrorMessage(actionError);
|
||||
setError(nextError);
|
||||
setMessage("");
|
||||
toast.error(nextError);
|
||||
} finally {
|
||||
setOfflineDraftSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const actionMutation = useMutation({
|
||||
mutationFn: async (action: MobileAction) => {
|
||||
if (!canUseRemote) {
|
||||
throw new Error("当前离线,请使用离线草稿动作");
|
||||
}
|
||||
if (!selectedTaskId) {
|
||||
throw new Error("请选择任务");
|
||||
}
|
||||
@@ -210,28 +261,9 @@ export function MobileApprovalsPage() {
|
||||
throw new Error("当前待办不允许该动作");
|
||||
}
|
||||
|
||||
if (!canUseRemote) {
|
||||
if (action === "return" || action === "transfer") {
|
||||
throw new Error("离线仅支持通过 / 驳回草稿,退回与转审需联网执行");
|
||||
}
|
||||
if (action === "reject" && !comment.trim()) {
|
||||
throw new Error("驳回必须填写意见");
|
||||
}
|
||||
|
||||
await enqueueWorkflowActionDraft({
|
||||
taskId: selectedTaskId,
|
||||
action,
|
||||
comment: action === "reject" ? comment.trim() : null,
|
||||
ownerEmployeeId,
|
||||
detail: selectedTask,
|
||||
});
|
||||
|
||||
return { mode: "queued" as const, action };
|
||||
}
|
||||
|
||||
if (action === "approve") {
|
||||
await approveWorkflowTask(selectedTaskId);
|
||||
return { mode: "submitted" as const, action };
|
||||
return { action };
|
||||
}
|
||||
|
||||
if (action === "reject") {
|
||||
@@ -239,7 +271,7 @@ export function MobileApprovalsPage() {
|
||||
throw new Error("驳回必须填写意见");
|
||||
}
|
||||
await rejectWorkflowTask(selectedTaskId, comment.trim());
|
||||
return { mode: "submitted" as const, action };
|
||||
return { action };
|
||||
}
|
||||
|
||||
if (action === "return") {
|
||||
@@ -247,7 +279,7 @@ export function MobileApprovalsPage() {
|
||||
throw new Error("退回必须填写意见");
|
||||
}
|
||||
await returnWorkflowTask(selectedTaskId, comment.trim());
|
||||
return { mode: "submitted" as const, action };
|
||||
return { action };
|
||||
}
|
||||
|
||||
if (!transferToApproverId) {
|
||||
@@ -258,17 +290,9 @@ export function MobileApprovalsPage() {
|
||||
}
|
||||
|
||||
await transferWorkflowTask(selectedTaskId, transferToApproverId, transferReason.trim());
|
||||
return { mode: "submitted" as const, action };
|
||||
return { action };
|
||||
},
|
||||
onSuccess: async (result) => {
|
||||
if (result.mode === "queued") {
|
||||
await queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "draft-queue", ownerEmployeeId] });
|
||||
setError("");
|
||||
setMessage(result.action === "approve" ? "当前离线,通过草稿已加入待同步队列。" : "当前离线,驳回草稿已加入待同步队列。");
|
||||
toast.success(result.action === "approve" ? "已加入通过草稿队列" : "已加入驳回草稿队列");
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "tasks"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "task-detail", selectedTaskId] }),
|
||||
@@ -289,6 +313,8 @@ export function MobileApprovalsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const isActionSubmitting = actionMutation.isPending || offlineDraftSubmitting;
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: async (items: WorkflowDraftQueueItem[]) => {
|
||||
let succeeded = 0;
|
||||
@@ -524,16 +550,28 @@ export function MobileApprovalsPage() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
data-testid="mobile-approvals-approve"
|
||||
disabled={actionMutation.isPending || !selectedTask.allowedActions.includes("approve")}
|
||||
onClick={() => actionMutation.mutate("approve")}
|
||||
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("approve")}
|
||||
onClick={() => {
|
||||
if (canUseRemote) {
|
||||
actionMutation.mutate("approve");
|
||||
return;
|
||||
}
|
||||
void queueOfflineDraft("approve");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{canUseRemote ? "通过" : "离线加入通过草稿"}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="mobile-approvals-reject"
|
||||
disabled={actionMutation.isPending || !selectedTask.allowedActions.includes("reject")}
|
||||
onClick={() => actionMutation.mutate("reject")}
|
||||
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("reject")}
|
||||
onClick={() => {
|
||||
if (canUseRemote) {
|
||||
actionMutation.mutate("reject");
|
||||
return;
|
||||
}
|
||||
void queueOfflineDraft("reject");
|
||||
}}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
@@ -541,7 +579,7 @@ export function MobileApprovalsPage() {
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="mobile-approvals-return"
|
||||
disabled={actionMutation.isPending || !canUseRemote || !selectedTask.allowedActions.includes("return")}
|
||||
disabled={isActionSubmitting || !canUseRemote || !selectedTask.allowedActions.includes("return")}
|
||||
onClick={() => actionMutation.mutate("return")}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -577,7 +615,7 @@ export function MobileApprovalsPage() {
|
||||
|
||||
<Button
|
||||
data-testid="mobile-approvals-transfer"
|
||||
disabled={actionMutation.isPending || !selectedTask.allowedActions.includes("transfer")}
|
||||
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("transfer")}
|
||||
onClick={() => actionMutation.mutate("transfer")}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -643,7 +681,7 @@ export function MobileApprovalsPage() {
|
||||
status: "queued",
|
||||
lastError: null,
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "draft-queue", ownerEmployeeId] });
|
||||
await refreshDraftQueueCache();
|
||||
if (canUseRemote) {
|
||||
syncMutation.mutate([{ ...item, status: "queued", lastError: null }]);
|
||||
}
|
||||
@@ -658,7 +696,7 @@ export function MobileApprovalsPage() {
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await removeWorkflowActionDraft(item.taskId, ownerEmployeeId);
|
||||
await queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "draft-queue", ownerEmployeeId] });
|
||||
await refreshDraftQueueCache();
|
||||
}}
|
||||
>
|
||||
丢弃草稿
|
||||
|
||||
在新工单中引用
屏蔽一个用户