修复非参与人报销只读边界(08-28-reimbursement-nonparticipant-readonly-projection)
这个提交包含在:
@@ -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
|
||||
|
||||
在新工单中引用
屏蔽一个用户