DG7/MRF7 移动信息架构与展示去重治理
这个提交包含在:
@@ -1,8 +1,11 @@
|
||||
import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchLeaveRequests, type LeaveRequestItem } from "../api/leave";
|
||||
import { fetchPurchaseRequests, type PurchaseRequestSummary } from "../api/purchaseRequests";
|
||||
import { fetchReimbursements, type ReimbursementListItem } from "../api/reimbursements";
|
||||
import { isApiError } from "../api/types";
|
||||
import {
|
||||
approveWorkflowTask,
|
||||
@@ -26,6 +29,9 @@ import { Spinner } from "../components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { MOBILE_APPROVAL_PERMISSIONS } from "../features/mobile/navigation";
|
||||
import { resolvePurchaseDisplayCode } from "../lib/approvalDisplay";
|
||||
import { businessPrimaryLabel, compactBusinessReference, joinBusinessMeta } from "../lib/businessDisplay";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import {
|
||||
cacheWorkflowTaskDetail,
|
||||
enqueueWorkflowActionDraft,
|
||||
@@ -61,6 +67,14 @@ type VisibleTaskCard = {
|
||||
detail: WorkflowTaskDetail | null;
|
||||
};
|
||||
|
||||
type EmployeeProgressCard = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
status: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const ENTITY_FILTER_OPTIONS: Array<{ value: EntityFilter; label: string }> = [
|
||||
{ value: "all", label: "全部" },
|
||||
{ value: "leave", label: "请假" },
|
||||
@@ -120,15 +134,8 @@ function businessStatusLabel(status: string | null | undefined) {
|
||||
return status ? labels[status] ?? status : "-";
|
||||
}
|
||||
|
||||
function isInternalId(value: string | null | undefined) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value ?? "");
|
||||
}
|
||||
|
||||
function publicReference(value: string | null | undefined) {
|
||||
if (!value || isInternalId(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
return compactBusinessReference(value);
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
@@ -209,13 +216,19 @@ function toVisibleRemoteTask(task: WorkflowTaskSummary): VisibleTaskCard {
|
||||
const summary = task.businessSummary;
|
||||
const reference = publicReference(summary?.reference ?? task.workflowInstance?.entityId);
|
||||
const metric = summary?.amount ?? summary?.duration ?? null;
|
||||
const title = businessPrimaryLabel({
|
||||
entityType,
|
||||
reference,
|
||||
title: summary?.title ?? task.stepName,
|
||||
fallback: task.stepName,
|
||||
});
|
||||
const subtitle = task.status === "completed"
|
||||
? joinBusinessMeta([`节点:${task.stepName}`, `完成于 ${formatDateTime(task.completedAt)}`])
|
||||
: joinBusinessMeta([`节点:${task.stepName}`, summary?.applicantName ? `申请人:${summary.applicantName}` : null]);
|
||||
return {
|
||||
id: task.id,
|
||||
title: summary?.title ?? task.stepName,
|
||||
subtitle:
|
||||
task.status === "completed"
|
||||
? `业务:${entityLabel(entityType)} · ${reference ?? "未生成业务编号"} · 完成于 ${formatDateTime(task.completedAt)}`
|
||||
: `业务:${entityLabel(entityType)} · ${reference ?? "未生成业务编号"}`,
|
||||
title,
|
||||
subtitle,
|
||||
status: task.status,
|
||||
entityType,
|
||||
entityId: task.workflowInstance?.entityId ?? null,
|
||||
@@ -252,11 +265,66 @@ function matchesSearch(card: VisibleTaskCard, keyword: string) {
|
||||
return haystack.includes(searchValue);
|
||||
}
|
||||
|
||||
function toEmployeeProgressCards(input: {
|
||||
reimbursements: ReimbursementListItem[];
|
||||
leaveRequests: LeaveRequestItem[];
|
||||
purchaseRequests: PurchaseRequestSummary[];
|
||||
}): EmployeeProgressCard[] {
|
||||
const reimbursementCards = input.reimbursements.map((item) => ({
|
||||
id: `reimbursement-${item.id}`,
|
||||
title: businessPrimaryLabel({
|
||||
entityType: "reimbursement",
|
||||
reference: item.formNumber,
|
||||
title: resolveReimbursementDisplayCode(item),
|
||||
}),
|
||||
subtitle: joinBusinessMeta([
|
||||
item.owner?.name ? `报销人:${item.owner.name}` : null,
|
||||
item.totalAmount ? `金额:${item.totalAmount}` : null,
|
||||
item.pendingWorkflow?.task?.stepName ? `节点:${item.pendingWorkflow.task.stepName}` : null,
|
||||
]),
|
||||
status: item.status,
|
||||
to: `/mobile/reimbursements/${encodeURIComponent(item.id)}`,
|
||||
}));
|
||||
|
||||
const leaveCards = input.leaveRequests.map((item) => ({
|
||||
id: `leave-${item.id}`,
|
||||
title: joinBusinessMeta(["请假", item.leaveType?.name]) || "请假",
|
||||
subtitle: joinBusinessMeta([
|
||||
`${item.startDate} 至 ${item.endDate}`,
|
||||
`${item.requestedDays} 天`,
|
||||
item.pendingWorkflow?.task?.stepName ? `节点:${item.pendingWorkflow.task.stepName}` : null,
|
||||
]),
|
||||
status: item.status,
|
||||
to: `/mobile/leave/${encodeURIComponent(item.id)}`,
|
||||
}));
|
||||
|
||||
const purchaseCards = input.purchaseRequests.map((item) => ({
|
||||
id: `purchase-${item.id}`,
|
||||
title: businessPrimaryLabel({
|
||||
entityType: "purchase_request",
|
||||
reference: item.requestNo,
|
||||
title: resolvePurchaseDisplayCode(item),
|
||||
}),
|
||||
subtitle: joinBusinessMeta([
|
||||
item.applicant?.name ? `申请人:${item.applicant.name}` : null,
|
||||
item.totalAmount ? `金额:${item.totalAmount}` : null,
|
||||
item.updatedAt ? `更新:${formatDateTime(item.updatedAt)}` : null,
|
||||
]),
|
||||
status: item.status,
|
||||
to: `/mobile/purchase-requests/${encodeURIComponent(item.id)}`,
|
||||
}));
|
||||
|
||||
return [...reimbursementCards, ...leaveCards, ...purchaseCards].slice(0, 8);
|
||||
}
|
||||
|
||||
export function MobileApprovalsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const routeParams = useParams();
|
||||
const { me, permissions, accessToken, offlineAccessMode } = useAuth();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const deepLinkTaskId = searchParams.get("taskId")?.trim() ?? "";
|
||||
const [searchParams] = useSearchParams();
|
||||
const deepLinkTaskId = routeParams.taskId?.trim() || searchParams.get("taskId")?.trim() || "";
|
||||
const isPushDetail = Boolean(deepLinkTaskId);
|
||||
|
||||
const [selectedTaskId, setSelectedTaskId] = useState(deepLinkTaskId);
|
||||
const [taskTab, setTaskTab] = useState<MobileTaskTab>("pending");
|
||||
@@ -274,13 +342,30 @@ export function MobileApprovalsPage() {
|
||||
const [offlineDraftSubmitting, setOfflineDraftSubmitting] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
const autoSyncRequestedRef = useRef(false);
|
||||
const detailSectionRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const ownerEmployeeId = me?.id ?? null;
|
||||
const canAccessRemoteApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]);
|
||||
const canUseRemote = isOnline && Boolean(accessToken) && canAccessRemoteApprovals;
|
||||
const remoteTaskStatus = taskTab === "completed" ? "completed" : "pending";
|
||||
|
||||
const reimbursementProgressQuery = useQuery({
|
||||
enabled: !canAccessRemoteApprovals,
|
||||
queryKey: ["mobile", "approvals", "progress", "reimbursements"],
|
||||
queryFn: () => fetchReimbursements({ scope: "own", page: 1, pageSize: 5 }),
|
||||
});
|
||||
|
||||
const leaveProgressQuery = useQuery({
|
||||
enabled: !canAccessRemoteApprovals,
|
||||
queryKey: ["mobile", "approvals", "progress", "leave"],
|
||||
queryFn: () => fetchLeaveRequests({ scope: "own", page: 1, pageSize: 5 }),
|
||||
});
|
||||
|
||||
const purchaseProgressQuery = useQuery({
|
||||
enabled: !canAccessRemoteApprovals,
|
||||
queryKey: ["mobile", "approvals", "progress", "purchase"],
|
||||
queryFn: () => fetchPurchaseRequests({ mine: true, page: 1, pageSize: 5 }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTaskId(deepLinkTaskId);
|
||||
}, [deepLinkTaskId]);
|
||||
@@ -293,30 +378,8 @@ export function MobileApprovalsPage() {
|
||||
}
|
||||
}, [taskTab]);
|
||||
|
||||
function updateTaskSelection(taskId: string) {
|
||||
setSelectedTaskId(taskId);
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
if (taskId) {
|
||||
nextParams.set("taskId", taskId);
|
||||
} else {
|
||||
nextParams.delete("taskId");
|
||||
}
|
||||
setSearchParams(nextParams, { replace: true });
|
||||
}
|
||||
|
||||
function scrollDetailIntoViewOnMobile() {
|
||||
if (typeof window === "undefined" || !window.matchMedia("(max-width: 767px)").matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
detailSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
}
|
||||
|
||||
function handleTaskSelection(taskId: string) {
|
||||
updateTaskSelection(taskId);
|
||||
scrollDetailIntoViewOnMobile();
|
||||
navigate(`/mobile/approvals/${encodeURIComponent(taskId)}`);
|
||||
}
|
||||
|
||||
async function invalidateWorkflowQueries(taskId: string | null = selectedTaskId) {
|
||||
@@ -391,8 +454,12 @@ export function MobileApprovalsPage() {
|
||||
.filter((item) => item.detail.task.status === "pending")
|
||||
.map((item) => ({
|
||||
id: item.detail.task.id,
|
||||
title: item.taskSnapshot.stepName ?? item.detail.task.stepName,
|
||||
subtitle: `业务:${entityLabel(item.taskSnapshot.entityType ?? item.detail.workflowInstance?.entityType)} · 缓存于 ${formatDateTime(item.savedAt)}`,
|
||||
title: businessPrimaryLabel({
|
||||
entityType: item.taskSnapshot.entityType ?? item.detail.workflowInstance?.entityType,
|
||||
reference: item.taskSnapshot.reference,
|
||||
title: item.taskSnapshot.stepName ?? item.detail.task.stepName,
|
||||
}),
|
||||
subtitle: joinBusinessMeta([`节点:${item.taskSnapshot.stepName ?? item.detail.task.stepName}`, `缓存于 ${formatDateTime(item.savedAt)}`]),
|
||||
status: item.detail.task.status,
|
||||
entityType: item.taskSnapshot.entityType ?? item.detail.workflowInstance?.entityType ?? null,
|
||||
entityId: item.taskSnapshot.entityId,
|
||||
@@ -432,21 +499,50 @@ export function MobileApprovalsPage() {
|
||||
const selectedTask = detailQuery.data ?? cachedDetailQuery.data?.detail ?? selectedCachedTaskDetail;
|
||||
const selectedReference = selectedTask ? resolveWorkflowTaskReferenceSnapshot(selectedTask) : null;
|
||||
const selectedDisplayReference = selectedTaskCard?.reference ?? publicReference(selectedReference?.reference);
|
||||
const selectedDetailReference = selectedDisplayReference ??
|
||||
publicReference(selectedTask?.reimbursement?.formNumber) ??
|
||||
publicReference(selectedTask?.purchaseRequest?.requestNo) ??
|
||||
publicReference(selectedTask?.travelRequest?.requestNo);
|
||||
const selectedDetailTitle = selectedTask
|
||||
? businessPrimaryLabel({
|
||||
entityType: selectedTask.workflowInstance?.entityType,
|
||||
reference: selectedDetailReference,
|
||||
title: selectedTaskCard?.title ?? selectedTask.task.stepName,
|
||||
})
|
||||
: "";
|
||||
const selectedApplicantName = selectedTaskCard?.applicantName ??
|
||||
selectedTask?.reimbursement?.submitter?.name ??
|
||||
selectedTask?.leaveRequest?.owner?.name ??
|
||||
selectedTask?.purchaseRequest?.applicant?.name ??
|
||||
selectedTask?.travelRequest?.applicant?.name ??
|
||||
null;
|
||||
const selectedMetric = selectedTaskCard?.metric ??
|
||||
selectedTask?.reimbursement?.totalAmount ??
|
||||
(selectedTask?.leaveRequest ? `${selectedTask.leaveRequest.requestedDays} 天` : null) ??
|
||||
selectedTask?.purchaseRequest?.totalAmount ??
|
||||
selectedTask?.travelRequest?.estimatedAmount ??
|
||||
null;
|
||||
const selectedTaskAllowsActions = Boolean(taskTab === "pending" && selectedTask?.task.status === "pending");
|
||||
const isOfflineUncachedDeepLink = Boolean(selectedTaskId) && !selectedTask && !canUseRemote;
|
||||
const employeeProgressCards = useMemo(
|
||||
() =>
|
||||
toEmployeeProgressCards({
|
||||
reimbursements: reimbursementProgressQuery.data?.items ?? [],
|
||||
leaveRequests: leaveProgressQuery.data?.items ?? [],
|
||||
purchaseRequests: purchaseProgressQuery.data?.items ?? [],
|
||||
}),
|
||||
[leaveProgressQuery.data?.items, purchaseProgressQuery.data?.items, reimbursementProgressQuery.data?.items],
|
||||
);
|
||||
const employeeProgressLoading =
|
||||
reimbursementProgressQuery.isLoading || leaveProgressQuery.isLoading || purchaseProgressQuery.isLoading;
|
||||
const employeeProgressError =
|
||||
reimbursementProgressQuery.error ?? leaveProgressQuery.error ?? purchaseProgressQuery.error ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
const visibleIds = new Set(filteredTaskCards.map((task) => task.id));
|
||||
setSelectedBatchTaskIds((current) => current.filter((taskId) => visibleIds.has(taskId)));
|
||||
}, [filteredTaskCards]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTaskId || !filteredTaskCards.length) {
|
||||
return;
|
||||
}
|
||||
updateTaskSelection(filteredTaskCards[0].id);
|
||||
}, [filteredTaskCards, selectedTaskId]);
|
||||
|
||||
const canTransfer = Boolean(
|
||||
canUseRemote &&
|
||||
selectedTaskAllowsActions &&
|
||||
@@ -566,7 +662,7 @@ export function MobileApprovalsPage() {
|
||||
setTransferToApproverId("");
|
||||
setError("");
|
||||
setMessage("");
|
||||
updateTaskSelection("");
|
||||
navigate("/mobile/approvals", { replace: true });
|
||||
toast.success("操作成功");
|
||||
},
|
||||
onError: (actionError) => {
|
||||
@@ -656,7 +752,7 @@ export function MobileApprovalsPage() {
|
||||
setError(result.failures.length ? result.failures.slice(0, 3).join(";") : "");
|
||||
setMessage(summary);
|
||||
if (canUseRemote && result.taskIds.includes(selectedTaskId)) {
|
||||
updateTaskSelection("");
|
||||
navigate("/mobile/approvals", { replace: true });
|
||||
}
|
||||
|
||||
if (result.failed > 0) {
|
||||
@@ -793,11 +889,16 @@ export function MobileApprovalsPage() {
|
||||
return (
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageHeader
|
||||
title="移动审批"
|
||||
subtitle={deepLinkTaskId ? "已定位指定待办。" : "处理待办。"}
|
||||
title={isPushDetail ? "审批详情" : canAccessRemoteApprovals ? "审批" : "审批进度"}
|
||||
subtitle={isPushDetail ? "查看并处理指定待办。" : canAccessRemoteApprovals ? "待我审批和已处理。" : "查看我发起的申请进度。"}
|
||||
actions={isPushDetail ? (
|
||||
<Button asChild size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/approvals">返回</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
/>
|
||||
|
||||
{!isOnline ? (
|
||||
{!isOnline && canAccessRemoteApprovals ? (
|
||||
<p className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
|
||||
当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存待办,并保存通过 / 驳回草稿;联网同步成功后才会正式提交。
|
||||
</p>
|
||||
@@ -811,6 +912,43 @@ export function MobileApprovalsPage() {
|
||||
<p className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{message}</p>
|
||||
) : null}
|
||||
|
||||
{!isPushDetail && !canAccessRemoteApprovals ? (
|
||||
<section className="mobile-card space-y-3" data-testid="mobile-approvals-progress">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">我发起的</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500">报销、请假、采购进度</p>
|
||||
</div>
|
||||
<span className="badge-neutral">{employeeProgressCards.length} 条</span>
|
||||
</div>
|
||||
|
||||
{employeeProgressLoading ? <div className="mobile-empty-state">加载中...</div> : null}
|
||||
{employeeProgressError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{resolveErrorMessage(employeeProgressError)}
|
||||
</p>
|
||||
) : null}
|
||||
{!employeeProgressLoading && !employeeProgressError && !employeeProgressCards.length ? (
|
||||
<div className="mobile-empty-state">暂无申请进度</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
{employeeProgressCards.map((item) => (
|
||||
<Link className="mobile-list-card block" data-testid="mobile-approval-progress-card" key={item.id} to={item.to}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-zinc-900">{item.title}</div>
|
||||
{item.subtitle ? <div className="mt-1 truncate text-xs text-zinc-500">{item.subtitle}</div> : null}
|
||||
</div>
|
||||
<StatusBadge status={item.status} />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{!isPushDetail && canAccessRemoteApprovals ? (
|
||||
<section className="mobile-card space-y-4" data-testid="mobile-approvals-list">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
@@ -986,9 +1124,8 @@ export function MobileApprovalsPage() {
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
{task.reference ? <div className="mt-1 text-xs text-zinc-500">业务单号:{task.reference}</div> : null}
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
申请人:{task.applicantName ?? "-"} · 金额/天数:{task.metric ?? "-"} · 业务状态:{businessStatusLabel(task.businessStatus)}
|
||||
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
|
||||
</div>
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
</button>
|
||||
@@ -1011,9 +1148,8 @@ export function MobileApprovalsPage() {
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
{task.reference ? <div className="mt-1 text-xs text-zinc-500">业务单号:{task.reference}</div> : null}
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
申请人:{task.applicantName ?? "-"} · 金额/天数:{task.metric ?? "-"} · 业务状态:{businessStatusLabel(task.businessStatus)}
|
||||
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
|
||||
</div>
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
</button>
|
||||
@@ -1021,8 +1157,10 @@ export function MobileApprovalsPage() {
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section ref={detailSectionRef} className="mobile-card scroll-mt-4 space-y-3" data-testid="mobile-approvals-detail">
|
||||
{isPushDetail ? (
|
||||
<section className="mobile-card scroll-mt-4 space-y-3" data-testid="mobile-approvals-detail">
|
||||
<h3 className="text-base font-semibold text-zinc-900">处理</h3>
|
||||
|
||||
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
|
||||
@@ -1046,44 +1184,37 @@ export function MobileApprovalsPage() {
|
||||
{selectedTask ? (
|
||||
<>
|
||||
<div className="mobile-list-card text-sm">
|
||||
<div>任务状态:{workflowStatusLabel(selectedTask.task.status)}</div>
|
||||
<div>步骤:{selectedTask.task.stepName}</div>
|
||||
<div>对象类型:{entityLabel(selectedTask.workflowInstance?.entityType)}</div>
|
||||
<div>业务单号:{selectedDisplayReference ?? "-"}</div>
|
||||
<div>业务状态:{businessStatusLabel(selectedTaskCard?.businessStatus ?? selectedTask.workflowInstance?.currentStatus)}</div>
|
||||
<div>申请人:{selectedTaskCard?.applicantName ?? "-"}</div>
|
||||
<div>金额/天数:{selectedTaskCard?.metric ?? "-"}</div>
|
||||
<div>创建时间:{formatDateTime(selectedTask.task.createdAt)}</div>
|
||||
<div>完成时间:{formatDateTime(selectedTask.task.completedAt)}</div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-semibold text-zinc-900">{selectedDetailTitle}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}</div>
|
||||
</div>
|
||||
<StatusBadge status={selectedTask.task.status} label={workflowStatusLabel(selectedTask.task.status)} />
|
||||
</div>
|
||||
<div className="mt-3 space-y-1 text-zinc-600">
|
||||
<div>{joinBusinessMeta([`类型:${entityLabel(selectedTask.workflowInstance?.entityType)}`, `业务状态:${businessStatusLabel(selectedTaskCard?.businessStatus ?? selectedTask.workflowInstance?.currentStatus)}`])}</div>
|
||||
<div>{joinBusinessMeta([selectedApplicantName ? `申请人:${selectedApplicantName}` : null, selectedMetric ? `金额/天数:${selectedMetric}` : null])}</div>
|
||||
{selectedTask.task.completedAt ? <div>完成时间:{formatDateTime(selectedTask.task.completedAt)}</div> : null}
|
||||
</div>
|
||||
{selectedTask.reimbursement ? (
|
||||
<>
|
||||
<div>报销单号:{selectedTask.reimbursement.formNumber ?? "-"}</div>
|
||||
<div>金额:{selectedTask.reimbursement.totalAmount ?? "-"}</div>
|
||||
<div>提交人:{selectedTask.reimbursement.submitter?.name ?? "-"}</div>
|
||||
</>
|
||||
<div className="mt-3 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
|
||||
{joinBusinessMeta([`报销日期:${selectedTask.reimbursement.reimbursementDate ?? "-"}`, selectedTask.reimbursement.description])}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedTask.leaveRequest ? (
|
||||
<>
|
||||
<div>假期类型:{selectedTask.leaveRequest.leaveType?.name ?? "-"}</div>
|
||||
<div>申请人:{selectedTask.leaveRequest.owner?.name ?? "-"}</div>
|
||||
<div>请假天数:{selectedTask.leaveRequest.requestedDays}</div>
|
||||
</>
|
||||
<div className="mt-3 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
|
||||
{joinBusinessMeta([`假期类型:${selectedTask.leaveRequest.leaveType?.name ?? "-"}`, `${selectedTask.leaveRequest.startDate ?? "-"} 至 ${selectedTask.leaveRequest.endDate ?? "-"}`])}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedTask.purchaseRequest ? (
|
||||
<>
|
||||
<div>采购单号:{selectedTask.purchaseRequest.requestNo ?? "-"}</div>
|
||||
<div>申请人:{selectedTask.purchaseRequest.applicant?.name ?? "-"}</div>
|
||||
<div>供应商:{selectedTask.purchaseRequest.supplier?.name ?? "-"}</div>
|
||||
<div>金额:{selectedTask.purchaseRequest.totalAmount ?? "-"}</div>
|
||||
</>
|
||||
<div className="mt-3 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
|
||||
{joinBusinessMeta([`供应商:${selectedTask.purchaseRequest.supplier?.name ?? "-"}`, selectedTask.purchaseRequest.title])}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedTask.travelRequest ? (
|
||||
<>
|
||||
<div>差旅单号:{selectedTask.travelRequest.requestNo ?? "-"}</div>
|
||||
<div>申请人:{selectedTask.travelRequest.applicant?.name ?? "-"}</div>
|
||||
<div>出差类型:{selectedTask.travelRequest.tripType === "international" ? "国际" : "国内"}</div>
|
||||
<div>预估金额:{selectedTask.travelRequest.estimatedAmount ?? "-"}</div>
|
||||
</>
|
||||
<div className="mt-3 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
|
||||
{joinBusinessMeta([`出差类型:${selectedTask.travelRequest.tripType === "international" ? "国际" : "国内"}`, selectedTask.travelRequest.purpose])}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1191,6 +1322,7 @@ export function MobileApprovalsPage() {
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mobile-card space-y-3" data-testid="mobile-approvals-queue">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
||||
在新工单中引用
屏蔽一个用户