优化报销审批证据布局(reimbursement-form-shared-component-migration)
这个提交包含在:
@@ -372,6 +372,7 @@ export function MobileReimbursementDetailPage() {
|
||||
mutationFn: (itemId: string) => deleteReimbursementItem(reimbursementId, itemId),
|
||||
onSuccess: async () => {
|
||||
await invalidateDetail();
|
||||
setItemForm(null);
|
||||
setConfirmDeleteItemId("");
|
||||
toast.success("明细已删除");
|
||||
},
|
||||
@@ -924,7 +925,6 @@ export function MobileReimbursementDetailPage() {
|
||||
const sourceItem = detail.items.find((candidate) => candidate.id === item.id);
|
||||
if (sourceItem) startEditItem(sourceItem);
|
||||
} : undefined,
|
||||
onDeleteItem: canMaintainReimbursement ? (item: { id: string }) => setConfirmDeleteItemId(item.id) : undefined,
|
||||
onLinkInvoice: canMaintainReimbursement ? (invoice: { id: string }, linkedItemId: string) => linkInvoiceItemMutation.mutate({ invoiceId: invoice.id, linkedItemId }) : undefined,
|
||||
linkInvoicePending: linkInvoiceItemMutation.isPending,
|
||||
linkInvoiceErrors: invoiceItemLinkErrors,
|
||||
@@ -961,7 +961,7 @@ export function MobileReimbursementDetailPage() {
|
||||
<label className="block space-y-1 text-sm text-zinc-700"><span>金额</span><Input disabled={itemSaveMutation.isPending} inputMode="decimal" value={itemForm.amount} onChange={(event) => setItemForm((current) => current ? { ...current, amount: event.target.value } : current)} /></label>
|
||||
<label className="block space-y-1 text-sm text-zinc-700"><span>说明</span><Textarea disabled={itemSaveMutation.isPending} rows={2} value={itemForm.description} onChange={(event) => setItemForm((current) => current ? { ...current, description: event.target.value } : current)} /></label>
|
||||
{shouldShowOfficeElectronicsHint(expenseTypes.find((type) => type.id === itemForm.expenseTypeId), itemForm.amount) ? <p className="text-xs text-amber-700">该金额通常应选择办公电子设备。</p> : null}
|
||||
<div className="grid grid-cols-2 gap-2"><Button disabled={!itemForm.expenseTypeId || !itemForm.expenseDate || !Number.isFinite(Number(itemForm.amount)) || Number(itemForm.amount) <= 0 || itemSaveMutation.isPending} onClick={() => itemSaveMutation.mutate(itemForm)} type="button">保存</Button><Button disabled={itemSaveMutation.isPending} onClick={() => setItemForm(null)} type="button" variant="secondary">取消</Button></div>
|
||||
<div className="grid grid-cols-2 gap-2"><Button disabled={!itemForm.expenseTypeId || !itemForm.expenseDate || !Number.isFinite(Number(itemForm.amount)) || Number(itemForm.amount) <= 0 || itemSaveMutation.isPending} onClick={() => itemSaveMutation.mutate(itemForm)} type="button">保存</Button><Button disabled={itemSaveMutation.isPending} onClick={() => setItemForm(null)} type="button" variant="secondary">取消</Button>{itemForm.mode === "edit" && itemForm.itemId ? <Button className="col-span-2" data-testid="reimbursement-item-delete" disabled={itemSaveMutation.isPending} onClick={() => setConfirmDeleteItemId(itemForm.itemId ?? "")} type="button" variant="destructive">删除明细</Button> : null}</div>
|
||||
</div>
|
||||
) : <p className="text-xs text-zinc-500">选择上方动作维护明细、发票或补充附件。</p>}
|
||||
</section>
|
||||
|
||||
@@ -327,6 +327,8 @@ export function ReimbursementDetailPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId, "precheck"] }),
|
||||
]);
|
||||
setItemEditor(null);
|
||||
setConfirmDeleteItemId("");
|
||||
setItemError("");
|
||||
toast.success("明细已删除");
|
||||
},
|
||||
@@ -1056,7 +1058,6 @@ export function ReimbursementDetailPage() {
|
||||
const sourceItem = detail.items.find((candidate) => candidate.id === item.id);
|
||||
if (sourceItem) startEditItem(sourceItem);
|
||||
} : undefined,
|
||||
onDeleteItem: canMaintainReimbursement ? (item: { id: string }) => setConfirmDeleteItemId(item.id) : undefined,
|
||||
onLinkInvoice: canMaintainReimbursement ? (invoice: { id: string }, linkedItemId: string) => linkInvoiceItemMutation.mutate({ invoiceId: invoice.id, linkedItemId }) : undefined,
|
||||
linkInvoicePending: linkInvoiceItemMutation.isPending,
|
||||
linkInvoiceErrors: invoiceItemLinkErrors,
|
||||
@@ -1081,7 +1082,7 @@ export function ReimbursementDetailPage() {
|
||||
{itemError ? <p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700" role="alert">{itemError}</p> : null}
|
||||
{canMaintainReimbursement ? (
|
||||
<SectionPanel title="维护报销内容" actions={<div className="flex flex-wrap gap-2"><Button data-testid="reimbursement-item-add" disabled={itemSaveMutation.isPending} onClick={startCreateItem} type="button" variant="secondary">新增明细</Button><Button data-testid={TEST_IDS.reimbursement.invoiceAdd} disabled={!canOpenInvoiceBindDialog} onClick={() => handleInvoiceDialogOpenChange(true)} type="button" variant="secondary">添加发票</Button><label className="inline-flex min-h-11 cursor-pointer items-center rounded-md border border-zinc-300 bg-white px-4 text-sm font-medium text-zinc-700" htmlFor="reimbursement-attachment-upload">选择附件</label><input className="hidden" id="reimbursement-attachment-upload" type="file" accept="application/pdf,image/jpeg,image/png,image/webp" multiple onChange={(event) => { const files = Array.from(event.target.files ?? []); if (files.length > MAX_REIMBURSEMENT_ATTACHMENT_FILES) { setSelectedAttachmentFiles([]); event.target.value = ""; toast.error(`单次最多上传 ${MAX_REIMBURSEMENT_ATTACHMENT_FILES} 个附件,请分批上传。`); return; } setSelectedAttachmentFiles(files); }} /><Button disabled={!selectedAttachmentFiles.length || uploadAttachmentsMutation.isPending} onClick={() => uploadAttachmentsMutation.mutate(selectedAttachmentFiles)} type="button" variant="secondary">{uploadAttachmentsMutation.isPending ? "上传中..." : selectedAttachmentFiles.length ? `上传 ${selectedAttachmentFiles.length} 个附件` : "上传附件"}</Button></div>}>
|
||||
{itemEditor ? <ReimbursementItemEditor mode={itemEditor.mode} expenseTypes={expenseTypes} expenseTypesLoading={expenseTypesQuery.isLoading} expenseTypesError={expenseTypesQuery.isError} departmentId={detail.departmentId} initialValues={editorInitialValues} onSave={handleItemSave} onCancel={handleItemCancel} isPending={itemSaveMutation.isPending} /> : <p className="text-sm text-zinc-500">选择上方动作维护明细、发票或补充附件。</p>}
|
||||
{itemEditor ? <ReimbursementItemEditor mode={itemEditor.mode} expenseTypes={expenseTypes} expenseTypesLoading={expenseTypesQuery.isLoading} expenseTypesError={expenseTypesQuery.isError} departmentId={detail.departmentId} initialValues={editorInitialValues} onSave={handleItemSave} onCancel={handleItemCancel} onDelete={itemEditor.mode === "edit" ? () => setConfirmDeleteItemId(itemEditor.itemId) : undefined} isPending={itemSaveMutation.isPending} /> : <p className="text-sm text-zinc-500">选择上方动作维护明细、发票或补充附件。</p>}
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface ReimbursementItemEditorProps {
|
||||
initialValues: ItemEditorInitialValues;
|
||||
onSave: (data: ItemEditorSaveData) => void;
|
||||
onCancel: () => void;
|
||||
onDelete?: () => void;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
@@ -44,6 +45,7 @@ export function ReimbursementItemEditor({
|
||||
initialValues,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
isPending,
|
||||
}: ReimbursementItemEditorProps) {
|
||||
const [expenseTypeId, setExpenseTypeId] = useState(initialValues.expenseTypeId);
|
||||
@@ -216,6 +218,11 @@ export function ReimbursementItemEditor({
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
{mode === "edit" && onDelete ? (
|
||||
<Button data-testid="reimbursement-item-delete" disabled={isPending} onClick={onDelete} type="button" variant="destructive">
|
||||
删除明细
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { StatusBadge } from "../../components/common/StatusBadge";
|
||||
import { useAttachmentDownloadReasonDialog } from "../../components/common/useAttachmentDownloadReasonDialog";
|
||||
import { ReimbursementDesktopWorkspaceShell } from "../../components/reimbursements/ReimbursementDesktopWorkspaceShell";
|
||||
import { ReimbursementMobileWorkspaceShell } from "../../components/reimbursements/ReimbursementMobileWorkspaceShell";
|
||||
import { ReimbursementReturnReferencePicker } from "../../components/reimbursements/ReimbursementReturnReferencePicker";
|
||||
import { BusinessDetailActionPanel } from "../../components/patterns/BusinessDetailTemplate";
|
||||
import { MobileStickyActionBar } from "../../components/patterns/MobileStickyActionBar";
|
||||
import { SectionPanel } from "../../components/patterns/SectionPanel";
|
||||
@@ -34,10 +35,7 @@ import { Textarea } from "../../components/ui/textarea";
|
||||
import { approvalRoleLabel, workflowActionLabel } from "../../lib/approvalDisplay";
|
||||
import { attachmentRequiresDownloadReason, type AttachmentGovernanceFields } from "../../lib/attachmentGovernance";
|
||||
import { formatDisplayDateTime } from "../../lib/displayFormat";
|
||||
import {
|
||||
buildReimbursementApprovalEvidence,
|
||||
buildReimbursementReturnReference,
|
||||
} from "../../lib/reimbursementApprovalProjection";
|
||||
import { buildReimbursementApprovalEvidence } from "../../lib/reimbursementApprovalProjection";
|
||||
import {
|
||||
buildReimbursementWorkspaceHeader,
|
||||
buildReimbursementWorkspaceIssueAnchors,
|
||||
@@ -295,6 +293,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
<Label htmlFor={`${layout}-reimbursement-return-comment`}>退回意见</Label>
|
||||
<p className="text-xs text-zinc-500">退回后原单可修改并重新提交,单号和审批历史会保留。</p>
|
||||
<Textarea data-testid={layout === "desktop" ? "workflow-task-return-comment" : "mobile-approvals-comment"} id={`${layout}-reimbursement-return-comment`} value={returnComment} onChange={(event) => setReturnComment(event.target.value)} />
|
||||
<ReimbursementReturnReferencePicker evidence={model.evidence} onReference={appendReturnReference} />
|
||||
<Button className="min-h-11 w-full" data-testid={layout === "desktop" ? "workflow-task-return" : "mobile-approvals-return-submit"} disabled={actionDisabled || !returnComment.trim()} onClick={() => requestAction("return")} type="button" variant="secondary">确认退回</Button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -341,8 +340,6 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
onOpenInvoice: (invoice: { id: string }) => openInvoiceMutation.mutate(invoice.id),
|
||||
onOpenAttachment: (attachment: { id: string }) => void openAttachment(attachment.id),
|
||||
onOpenMaterialBundle: hasMaterials ? () => void openAllMaterials() : undefined,
|
||||
onReferenceItem: (item: Parameters<typeof buildReimbursementReturnReference>[0]["item"]) => appendReturnReference(buildReimbursementReturnReference({ item })),
|
||||
onReferenceInvoice: (invoice: Parameters<typeof buildReimbursementReturnReference>[0]["invoice"]) => appendReturnReference(buildReimbursementReturnReference({ invoice })),
|
||||
};
|
||||
|
||||
const dialogs = (
|
||||
|
||||
在新工单中引用
屏蔽一个用户