R40: 完成档案编辑扩展与员工导出

这个提交包含在:
2026-03-28 17:42:09 +08:00
父节点 1cac1447b1
当前提交 2eff8e01a2
共修改 10 个文件,包含 654 行新增和 23 行删除
+101 -21
查看文件
@@ -10,6 +10,8 @@ import {
startEmployeeOffboarding,
transferEmployee,
} from "../api/employees";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
@@ -29,6 +31,7 @@ function errorMessage(error: unknown, fallback: string) {
export function EmployeeProfilePage() {
const { employeeId = "" } = useParams();
const { permissions, me } = useAuth();
const queryClient = useQueryClient();
const profileQuery = useQuery({
enabled: Boolean(employeeId),
@@ -37,7 +40,15 @@ export function EmployeeProfilePage() {
});
const profile = profileQuery.data;
const canManageEmployeeProfile = hasPermission(permissions, "system.users");
const canEditSelfProfile = me?.id === employeeId;
const canEditBaseFields = canManageEmployeeProfile || canEditSelfProfile;
const canEditExtendedFields = canManageEmployeeProfile;
const [form, setForm] = useState({
title: "",
hireDate: "",
probationEndDate: "",
lastWorkingDate: "",
workLocation: "",
emergencyContactName: "",
emergencyContactPhone: "",
@@ -47,6 +58,10 @@ export function EmployeeProfilePage() {
useEffect(() => {
if (!profile) return;
setForm({
title: profile.employee.title ?? "",
hireDate: profile.employee.hireDate ?? "",
probationEndDate: profile.employee.probationEndDate ?? "",
lastWorkingDate: profile.employee.lastWorkingDate ?? "",
workLocation: profile.employee.workLocation ?? "",
emergencyContactName: profile.employee.emergencyContactName ?? "",
emergencyContactPhone: profile.employee.emergencyContactPhone ?? "",
@@ -55,7 +70,25 @@ export function EmployeeProfilePage() {
}, [profile]);
const saveMutation = useMutation({
mutationFn: () => patchEmployeeProfile(employeeId, { ...form }),
mutationFn: () =>
patchEmployeeProfile(employeeId, {
...(canEditExtendedFields
? {
...(form.title.trim() ? { title: form.title.trim() } : {}),
hireDate: form.hireDate || null,
probationEndDate: form.probationEndDate || null,
lastWorkingDate: form.lastWorkingDate || null,
}
: {}),
...(canEditBaseFields
? {
workLocation: form.workLocation,
emergencyContactName: form.emergencyContactName,
emergencyContactPhone: form.emergencyContactPhone,
notes: form.notes,
}
: {}),
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] });
toast.success("档案已保存");
@@ -161,29 +194,76 @@ export function EmployeeProfilePage() {
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<label className="text-sm text-zinc-500">办公地点</label>
<Input value={form.workLocation} onChange={(event) => setForm((prev) => ({ ...prev, workLocation: event.target.value }))} />
{canEditExtendedFields ? (
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<label className="text-sm text-zinc-500">职位</label>
<Input value={form.title} onChange={(event) => setForm((prev) => ({ ...prev, title: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">入职日期</label>
<Input type="date" value={form.hireDate} onChange={(event) => setForm((prev) => ({ ...prev, hireDate: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">试用期截止</label>
<Input type="date" value={form.probationEndDate} onChange={(event) => setForm((prev) => ({ ...prev, probationEndDate: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">离职日期</label>
<Input type="date" value={form.lastWorkingDate} onChange={(event) => setForm((prev) => ({ ...prev, lastWorkingDate: event.target.value }))} />
</div>
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">紧急联系人</label>
<Input value={form.emergencyContactName} onChange={(event) => setForm((prev) => ({ ...prev, emergencyContactName: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">紧急联系电话</label>
<Input value={form.emergencyContactPhone} onChange={(event) => setForm((prev) => ({ ...prev, emergencyContactPhone: event.target.value }))} />
</div>
</div>
) : null}
<div className="grid gap-2">
<label className="text-sm text-zinc-500">备注</label>
<Textarea value={form.notes} onChange={(event) => setForm((prev) => ({ ...prev, notes: event.target.value }))} />
</div>
{canEditBaseFields ? (
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<label className="text-sm text-zinc-500">办公地点</label>
<Input value={form.workLocation} onChange={(event) => setForm((prev) => ({ ...prev, workLocation: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">紧急联系人</label>
<Input value={form.emergencyContactName} onChange={(event) => setForm((prev) => ({ ...prev, emergencyContactName: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">紧急联系电话</label>
<Input value={form.emergencyContactPhone} onChange={(event) => setForm((prev) => ({ ...prev, emergencyContactPhone: event.target.value }))} />
</div>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2">
<div>
<div className="text-sm text-zinc-500">办公地点</div>
<div className="mt-2 text-sm text-zinc-900">{profile.employee.workLocation ?? "-"}</div>
</div>
<div>
<div className="text-sm text-zinc-500">紧急联系人</div>
<div className="mt-2 text-sm text-zinc-900">{profile.employee.emergencyContactName ?? "-"}</div>
</div>
<div>
<div className="text-sm text-zinc-500">紧急联系电话</div>
<div className="mt-2 text-sm text-zinc-900">{profile.employee.emergencyContactPhone ?? "-"}</div>
</div>
</div>
)}
<div className="flex justify-end">
<Button type="button" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>保存档案</Button>
</div>
{canEditBaseFields ? (
<div className="grid gap-2">
<label className="text-sm text-zinc-500">备注</label>
<Textarea value={form.notes} onChange={(event) => setForm((prev) => ({ ...prev, notes: event.target.value }))} />
</div>
) : (
<div>
<div className="text-sm text-zinc-500">备注</div>
<div className="mt-2 whitespace-pre-wrap text-sm text-zinc-900">{profile.employee.notes ?? "-"}</div>
</div>
)}
{canEditBaseFields ? (
<div className="flex justify-end">
<Button type="button" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>保存档案</Button>
</div>
) : null}
</div>
<div className="space-y-6">