实现报销材料合并预览与差旅概要(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateReimbursementTravelSummary, type ReimbursementDetail, type ReimbursementTravelSummaryPayload } from "../../api/reimbursements";
|
||||
import { resolveErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { isTravelReimbursement } from "../../lib/reimbursementTravel";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
type Props = { detail: ReimbursementDetail; reimbursementId: string; editable: boolean; mobile?: boolean };
|
||||
const emptyForm: ReimbursementTravelSummaryPayload = { travelPurpose: null, travelLocation: null, travelStartDate: null, travelEndDate: null };
|
||||
|
||||
export function ReimbursementTravelSummary({ detail, reimbursementId, editable, mobile = false }: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const isTravel = isTravelReimbursement(detail);
|
||||
useEffect(() => {
|
||||
setForm({
|
||||
travelPurpose: detail.travelSummary?.purpose ?? null,
|
||||
travelLocation: detail.travelSummary?.location ?? null,
|
||||
travelStartDate: detail.travelSummary?.startDate ?? null,
|
||||
travelEndDate: detail.travelSummary?.endDate ?? null,
|
||||
});
|
||||
}, [detail.travelSummary]);
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => updateReimbursementTravelSummary(reimbursementId, form),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] });
|
||||
setEditing(false);
|
||||
toast.success("差旅概要已保存");
|
||||
},
|
||||
onError: (error) => toast.error(resolveErrorMessage(error) || "差旅概要保存失败"),
|
||||
});
|
||||
if (!isTravel) return null;
|
||||
const summary = detail.travelSummary ?? { purpose: null, location: null, startDate: null, endDate: null };
|
||||
return (
|
||||
<section className={mobile ? "mobile-card" : "rounded-xl border border-zinc-200 bg-white p-5"} data-testid={mobile ? "mobile-reimbursement-travel-summary" : "reimbursement-travel-summary"}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-semibold text-zinc-900">差旅概要</h2>
|
||||
{editable && !editing ? <Button size="sm" type="button" variant="secondary" onClick={() => setEditing(true)}>编辑</Button> : null}
|
||||
</div>
|
||||
{editing ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
<label className="block space-y-1 text-sm text-zinc-700"><span>出差事由</span><Input value={form.travelPurpose ?? ""} onChange={(event) => setForm((current) => ({ ...current, travelPurpose: event.target.value || null }))} /></label>
|
||||
<label className="block space-y-1 text-sm text-zinc-700"><span>出差地点</span><Input value={form.travelLocation ?? ""} onChange={(event) => setForm((current) => ({ ...current, travelLocation: event.target.value || null }))} /></label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="block space-y-1 text-sm text-zinc-700"><span>开始日期</span><Input type="date" value={form.travelStartDate ?? ""} onChange={(event) => setForm((current) => ({ ...current, travelStartDate: event.target.value || null }))} /></label>
|
||||
<label className="block space-y-1 text-sm text-zinc-700"><span>结束日期</span><Input type="date" value={form.travelEndDate ?? ""} onChange={(event) => setForm((current) => ({ ...current, travelEndDate: event.target.value || null }))} /></label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button type="button" disabled={mutation.isPending} onClick={() => mutation.mutate()}>{mutation.isPending ? "保存中…" : "保存"}</Button>
|
||||
<Button type="button" variant="secondary" disabled={mutation.isPending} onClick={() => setEditing(false)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<dl className="mt-3 grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
|
||||
<div><dt className="text-xs text-zinc-500">出差事由</dt><dd className="mt-1 text-zinc-900">{summary.purpose || "未填写"}</dd></div>
|
||||
<div><dt className="text-xs text-zinc-500">出差地点</dt><dd className="mt-1 text-zinc-900">{summary.location || "未填写"}</dd></div>
|
||||
<div><dt className="text-xs text-zinc-500">开始日期</dt><dd className="mt-1 text-zinc-900">{summary.startDate || "未填写"}</dd></div>
|
||||
<div><dt className="text-xs text-zinc-500">结束日期</dt><dd className="mt-1 text-zinc-900">{summary.endDate || "未填写"}</dd></div>
|
||||
</dl>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户