修复非参与人报销只读边界(08-28-reimbursement-nonparticipant-readonly-projection)

这个提交包含在:
2026-08-29 08:22:23 +08:00
父节点 174cb7acde
当前提交 e8d676634f
共修改 24 个文件,包含 891 行新增和 62 行删除
@@ -198,12 +198,12 @@ export function MobileReimbursementDetailPage() {
const detail = detailQuery.data;
const canUploadInvoice = hasPermission(permissions, "invoice.upload");
const canViewOwnInvoice = hasPermission(permissions, "invoice.view_own");
const canViewAllExpense = hasPermission(permissions, "expense.view_all");
const canDeleteExpense = hasPermission(permissions, "expense.delete_own");
const canSubmitExpense = hasPermission(permissions, "expense.submit");
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
const isParticipant = Boolean(detail && me?.id && [detail.ownerId, detail.submitterId].includes(me.id));
const canEdit = isEditableStatus(detail?.status);
const canMaintainReimbursement = Boolean(canEdit && isParticipant);
const pendingTaskId = detail?.pendingWorkflow?.task?.id ?? null;
const canRemindPendingWorkflow = Boolean(
detail?.status === "reviewing" &&
@@ -211,14 +211,14 @@ export function MobileReimbursementDetailPage() {
isParticipant &&
detail?.pendingWorkflow?.task?.assignee_id !== me?.id,
);
const canOpenInvoiceDialog = Boolean(canEdit && isParticipant && canUploadInvoice && canViewOwnInvoice);
const canUnbindInvoices = Boolean(canEdit && (isParticipant || canViewAllExpense));
const canDeleteDraft = Boolean(detail?.status === "draft" && canDeleteExpense && (isParticipant || canViewAllExpense));
const canManageAttachments = Boolean(canEdit && (isParticipant || canViewAllExpense));
const canOpenInvoiceDialog = Boolean(canMaintainReimbursement && canUploadInvoice && canViewOwnInvoice);
const canUnbindInvoices = canMaintainReimbursement;
const canDeleteDraft = Boolean(detail?.status === "draft" && canDeleteExpense && isParticipant);
const canManageAttachments = canMaintainReimbursement;
const selectedInvoiceEntries = Object.entries(selectedInvoices);
const reimbursementPrecheckQuery = useQuery({
enabled: Boolean(reimbursementId && canSubmitExpense && detail),
enabled: Boolean(reimbursementId && canSubmitExpense && canMaintainReimbursement),
queryKey: ["reimbursements", reimbursementId, "precheck"],
queryFn: () => precheckReimbursement(reimbursementId),
});
@@ -834,10 +834,10 @@ export function MobileReimbursementDetailPage() {
const resolveLinkedItemLabel = (linkedItemId: string | null) => {
if (!linkedItemId) {
return "自动生成或匹配明细";
return "未关联";
}
const item = detail?.items.find((candidate) => candidate.id === linkedItemId);
return item ? itemLabel(item) : "明细";
return item ? itemLabel(item) : "无法识别";
};
const expenseTypes = expenseTypesQuery.data?.items ?? [];
@@ -874,7 +874,7 @@ export function MobileReimbursementDetailPage() {
}
const displayCode = resolveReimbursementDisplayCode(detail);
const showSubmitAction = Boolean(canEdit && canSubmitExpense);
const showSubmitAction = Boolean(canMaintainReimbursement && canSubmitExpense);
const showStickyActions = showSubmitAction;
const showSecondaryActions = Boolean(actionError || canDeleteDraft);
@@ -903,6 +903,16 @@ export function MobileReimbursementDetailPage() {
<div className="mobile-section-header mobile-section-header--top">
<div>
<h2 className="mobile-section-title">报销概况</h2>
{!isParticipant ? (
<p className="mt-1 text-xs text-zinc-500">
只读查看
{detail.status === "draft"
? " · 等待报销人完善"
: detail.status === "rejected"
? " · 等待报销人修改后重提"
: ""}
</p>
) : null}
</div>
</div>
<div data-testid="mobile-reimbursement-summary">
@@ -990,7 +1000,7 @@ export function MobileReimbursementDetailPage() {
<section className="mobile-card" data-testid="mobile-reimbursement-items">
<div className="flex items-center justify-between gap-3">
<h2 className="text-base font-semibold text-zinc-900">报销明细</h2>
{canEdit ? (
{canMaintainReimbursement ? (
<Button size="sm" variant="secondary" type="button" onClick={startCreateItem}>
新增
</Button>
@@ -1076,7 +1086,7 @@ export function MobileReimbursementDetailPage() {
<div className="shrink-0 text-base font-semibold text-zinc-900">{formatMoney(item.amount)}</div>
</div>
{item.description ? <p className="mt-2 text-sm leading-5 text-zinc-600">{item.description}</p> : null}
{canEdit ? (
{canMaintainReimbursement ? (
<div className="mobile-two-action-grid mt-3">
<Button size="sm" variant="secondary" type="button" onClick={() => startEditItem(item)}>
编辑
@@ -1220,7 +1230,9 @@ export function MobileReimbursementDetailPage() {
<div className="mt-3 space-y-2">
<div data-testid={TEST_IDS.reimbursement.invoices} className="space-y-2">
{detail.invoices.map((invoice) => {
const invoiceHint = resolveInvoiceNextStepText(invoice);
const invoiceHint = invoice.status === "bound" && !canMaintainReimbursement
? "已关联"
: resolveInvoiceNextStepText(invoice);
return (
<article key={invoice.id} className="mobile-list-card">
<button