文件
hyb-oa/client/src/pages/EmployeeProfilePage.tsx
T

731 行
33 KiB
TypeScript

import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
completeEmployeeChecklistTask,
createEmployeeContract,
fetchEmployeeContracts,
fetchEmployeeProfile,
patchEmployeeContract,
patchEmployeeProfile,
regularizeEmployee,
skipEmployeeChecklistTask,
startEmployeeOffboarding,
transferEmployee,
uploadEmployeeAvatar,
type EmployeeContract,
type EmployeeContractType,
} from "../api/employees";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
import { Textarea } from "../components/ui/textarea";
import { toast } from "sonner";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
type ProfileTab = "profile" | "contracts";
type ContractFormState = {
contractNo: string;
contractType: EmployeeContractType;
startDate: string;
endDate: string;
terminatedDate: string;
attachmentPath: string;
};
const CONTRACT_TYPE_OPTIONS: Array<{ value: EmployeeContractType; label: string }> = [
{ value: "labor", label: "劳动合同" },
{ value: "nda", label: "保密协议" },
{ value: "non_compete", label: "竞业限制" },
{ value: "internship", label: "实习协议" },
];
function errorMessage(error: unknown, fallback: string) {
if (isApiError(error)) {
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
}
return error instanceof Error ? error.message : fallback;
}
function buildEmptyContractForm(): ContractFormState {
return {
contractNo: "",
contractType: "labor",
startDate: "",
endDate: "",
terminatedDate: "",
attachmentPath: "",
};
}
function contractTypeLabel(type: EmployeeContractType) {
return CONTRACT_TYPE_OPTIONS.find((item) => item.value === type)?.label ?? type;
}
export function EmployeeProfilePage() {
const { employeeId = "" } = useParams();
const { permissions, me } = useAuth();
const queryClient = useQueryClient();
const profileQuery = useQuery({
enabled: Boolean(employeeId),
queryKey: ["employee-profile", employeeId],
queryFn: () => fetchEmployeeProfile(employeeId),
});
const canManageEmployeeProfile = hasPermission(permissions, "system.users");
const canEditSelfProfile = me?.id === employeeId;
const canEditBaseFields = canManageEmployeeProfile || canEditSelfProfile;
const canEditExtendedFields = canManageEmployeeProfile;
const canUploadAvatar = canManageEmployeeProfile || canEditSelfProfile;
const [activeTab, setActiveTab] = useState<ProfileTab>("profile");
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarInputKey, setAvatarInputKey] = useState(0);
const [editingContractId, setEditingContractId] = useState<string | null>(null);
const [contractForm, setContractForm] = useState<ContractFormState>(buildEmptyContractForm());
const [form, setForm] = useState({
title: "",
hireDate: "",
probationEndDate: "",
lastWorkingDate: "",
workLocation: "",
emergencyContactName: "",
emergencyContactPhone: "",
notes: "",
});
const contractsQuery = useQuery({
enabled: Boolean(employeeId) && canManageEmployeeProfile,
queryKey: ["employee-contracts", employeeId],
queryFn: () => fetchEmployeeContracts(employeeId),
});
useEffect(() => {
if (!profileQuery.data) {
return;
}
setForm({
title: profileQuery.data.employee.title ?? "",
hireDate: profileQuery.data.employee.hireDate ?? "",
probationEndDate: profileQuery.data.employee.probationEndDate ?? "",
lastWorkingDate: profileQuery.data.employee.lastWorkingDate ?? "",
workLocation: profileQuery.data.employee.workLocation ?? "",
emergencyContactName: profileQuery.data.employee.emergencyContactName ?? "",
emergencyContactPhone: profileQuery.data.employee.emergencyContactPhone ?? "",
notes: profileQuery.data.employee.notes ?? "",
});
}, [profileQuery.data]);
useEffect(() => {
if (canManageEmployeeProfile || activeTab !== "contracts") {
return;
}
setActiveTab("profile");
}, [activeTab, canManageEmployeeProfile]);
useEffect(() => {
setAvatarFile(null);
setAvatarInputKey((current) => current + 1);
setEditingContractId(null);
setContractForm(buildEmptyContractForm());
}, [employeeId]);
const saveMutation = useMutation({
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("档案已保存");
},
onError: (error) => toast.error(errorMessage(error, "保存失败")),
});
const avatarMutation = useMutation({
mutationFn: async () => {
if (!avatarFile) {
throw new Error("请先选择头像文件");
}
return uploadEmployeeAvatar(employeeId, avatarFile);
},
onSuccess: async () => {
setAvatarFile(null);
setAvatarInputKey((current) => current + 1);
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] }),
queryClient.invalidateQueries({ queryKey: ["employees"] }),
queryClient.invalidateQueries({ queryKey: ["presence"] }),
queryClient.invalidateQueries({ queryKey: ["employee-organization"] }),
]);
toast.success("头像已更新");
},
onError: (error) => toast.error(errorMessage(error, "头像上传失败")),
});
const taskMutation = useMutation({
mutationFn: ({ taskId, action }: { taskId: string; action: "complete" | "skip" }) =>
action === "complete"
? completeEmployeeChecklistTask(employeeId, taskId)
: skipEmployeeChecklistTask(employeeId, taskId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] });
toast.success("清单已更新");
},
onError: (error) => toast.error(errorMessage(error, "更新清单失败")),
});
const regularizeMutation = useMutation({
mutationFn: () => regularizeEmployee(employeeId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] });
toast.success("已转正");
},
onError: (error) => toast.error(errorMessage(error, "转正失败")),
});
const transferMutation = useMutation({
mutationFn: () => transferEmployee(employeeId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] });
toast.success("已记录调岗事件");
},
onError: (error) => toast.error(errorMessage(error, "调岗失败")),
});
const offboardingMutation = useMutation({
mutationFn: () => startEmployeeOffboarding(employeeId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] });
toast.success("已发起离职");
},
onError: (error) => toast.error(errorMessage(error, "离职失败")),
});
const contractMutation = useMutation({
mutationFn: async () => {
const payload = {
contractNo: contractForm.contractNo.trim(),
contractType: contractForm.contractType,
startDate: contractForm.startDate,
endDate: contractForm.endDate,
terminatedDate: contractForm.terminatedDate || null,
attachmentPath: contractForm.attachmentPath.trim() ? contractForm.attachmentPath.trim() : null,
};
if (editingContractId) {
return patchEmployeeContract(employeeId, editingContractId, payload);
}
return createEmployeeContract(employeeId, payload);
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employee-contracts", employeeId] });
setEditingContractId(null);
setContractForm(buildEmptyContractForm());
toast.success(editingContractId ? "合同已更新" : "合同已新增");
},
onError: (error) => toast.error(errorMessage(error, editingContractId ? "更新合同失败" : "新增合同失败")),
});
if (profileQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载员工档案中...</div>;
}
if (profileQuery.isError || !profileQuery.data) {
return <div className="card-std p-6 text-sm text-red-600">{errorMessage(profileQuery.error, "加载员工档案失败")}</div>;
}
const profile = profileQuery.data;
const reportingChain = profile.organization.reportingChain.length
? profile.organization.reportingChain
: [
{
id: profile.employee.id,
employeeNo: profile.employee.employeeNo,
name: profile.employee.name,
title: profile.employee.title,
status: profile.employee.status,
avatarUrl: profile.employee.avatarUrl,
department: profile.employee.department,
},
];
const directReports = profile.organization.directReports;
const contracts = contractsQuery.data?.items ?? [];
function resetContractEditor() {
setEditingContractId(null);
setContractForm(buildEmptyContractForm());
}
function startEditingContract(contract: EmployeeContract) {
setEditingContractId(contract.id);
setContractForm({
contractNo: contract.contractNo,
contractType: contract.contractType,
startDate: contract.startDate ?? "",
endDate: contract.endDate ?? "",
terminatedDate: contract.terminatedDate ?? "",
attachmentPath: contract.attachmentPath ?? "",
});
}
return (
<div className="space-y-6">
<PageHeader
title={`${profile.employee.name} 档案`}
subtitle={`${profile.employee.employeeNo} · ${profile.employee.title}`}
actions={
canManageEmployeeProfile ? (
<div className="flex flex-wrap gap-2">
<Button type="button" variant="secondary" onClick={() => regularizeMutation.mutate()} disabled={regularizeMutation.isPending}>
转正
</Button>
<Button type="button" variant="secondary" onClick={() => transferMutation.mutate()} disabled={transferMutation.isPending}>
记录调岗
</Button>
<Button type="button" onClick={() => offboardingMutation.mutate()} disabled={offboardingMutation.isPending}>
发起离职
</Button>
</div>
) : null
}
/>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as ProfileTab)}>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
<TabsTrigger value="profile">档案</TabsTrigger>
{canManageEmployeeProfile ? <TabsTrigger value="contracts">合同</TabsTrigger> : null}
</TabsList>
<TabsContent value="profile" className="space-y-6">
<div className="grid gap-6 xl:grid-cols-3">
<div className="card-std space-y-6 p-6 xl:col-span-2">
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="flex items-center gap-4">
<EmployeeAvatar name={profile.employee.name} src={profile.employee.avatarUrl} size="xl" />
<div>
<div className="text-xl font-semibold text-zinc-900">{profile.employee.name}</div>
<div className="mt-1 text-sm text-zinc-500">
{profile.employee.employeeNo} · {profile.employee.email}
</div>
<div className="mt-3 flex flex-wrap gap-2">
<StatusBadge status={profile.employee.status} />
<StatusBadge status={profile.employee.employmentStatus} />
</div>
</div>
</div>
{canUploadAvatar ? (
<div className="w-full max-w-sm space-y-2">
<label className="text-sm text-zinc-500">头像上传</label>
<Input
key={avatarInputKey}
type="file"
accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp"
onChange={(event) => setAvatarFile(event.target.files?.[0] ?? null)}
/>
<div className="flex flex-wrap items-center gap-2">
<Button type="button" variant="secondary" onClick={() => avatarMutation.mutate()} disabled={!avatarFile || avatarMutation.isPending}>
{avatarMutation.isPending ? "上传中..." : "上传头像"}
</Button>
<div className="text-xs text-zinc-500">支持 jpg/png/webp,最大 2MB,自动裁剪为 200x200</div>
</div>
</div>
) : null}
</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.department?.name ?? "-"} / {profile.employee.directManager?.name ?? "-"}
</div>
</div>
<div>
<div className="text-sm text-zinc-500">手机 / 邮箱</div>
<div className="mt-2 text-sm text-zinc-900">
{profile.employee.phone} / {profile.employee.email}
</div>
</div>
<div>
<div className="text-sm text-zinc-500">入职 / 试用期</div>
<div className="mt-2 text-sm text-zinc-900">
{profile.employee.hireDate ?? "-"} / {profile.employee.probationEndDate ?? "-"}
</div>
</div>
<div>
<div className="text-sm text-zinc-500">离职日期</div>
<div className="mt-2 text-sm text-zinc-900">{profile.employee.lastWorkingDate ?? "-"}</div>
</div>
</div>
{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>
) : null}
{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>
)}
{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">
<div className="card-std space-y-4 p-6">
<h3 className="text-base font-semibold text-zinc-900">离职阻塞项</h3>
<div className="rounded-lg border border-zinc-200 p-3 text-sm">
资产未归还:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.openAssetCount}</span>
</div>
<div className="rounded-lg border border-zinc-200 p-3 text-sm">
待办审批任务:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.pendingWorkflowTaskCount}</span>
</div>
<div className="rounded-lg border border-zinc-200 p-3 text-sm">
可完成离职:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.canCompleteOffboarding ? "是" : "否"}</span>
</div>
</div>
<div className="card-std p-6">
<h3 className="text-base font-semibold text-zinc-900">汇报链</h3>
<div className="mt-4 space-y-3">
{reportingChain.map((item, index) => (
<div key={item.id} className="rounded-lg border border-zinc-200 p-3 text-sm">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<EmployeeAvatar name={item.name} src={item.avatarUrl} size="sm" />
<div>
<div className="font-medium text-zinc-900">{item.name}</div>
<div className="mt-1 text-zinc-500">
{item.employeeNo} · {item.title} · {item.department?.name ?? "-"}
</div>
</div>
</div>
<StatusBadge status={item.status} />
</div>
{index === reportingChain.length - 1 ? <div className="mt-2 text-xs text-zinc-500">当前员工</div> : null}
</div>
))}
</div>
</div>
<div className="card-std p-6">
<h3 className="text-base font-semibold text-zinc-900">直属下级</h3>
<div className="mt-4 space-y-3">
{directReports.length ? (
directReports.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 p-3 text-sm">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<EmployeeAvatar name={item.name} src={item.avatarUrl} size="sm" />
<div>
<div className="font-medium text-zinc-900">{item.name}</div>
<div className="mt-1 text-zinc-500">
{item.employeeNo} · {item.title} · {item.department?.name ?? "-"}
</div>
</div>
</div>
<StatusBadge status={item.status} />
</div>
</div>
))
) : (
<div className="text-sm text-zinc-500">暂无直属下级</div>
)}
</div>
</div>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<div className="card-std p-6">
<h3 className="text-base font-semibold text-zinc-900">生命周期事件</h3>
<div className="mt-4 space-y-3">
{profile.lifecycle.events.length ? (
profile.lifecycle.events.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 p-3 text-sm">
<div className="font-medium text-zinc-900">{item.eventType}</div>
<div className="mt-1 text-zinc-500">生效日期:{item.effectiveDate ?? "-"}</div>
</div>
))
) : (
<div className="text-sm text-zinc-500">暂无生命周期事件</div>
)}
</div>
</div>
<div className="card-std p-6">
<h3 className="text-base font-semibold text-zinc-900">名下资产</h3>
<div className="mt-4 space-y-3">
{profile.assignedAssets.length ? (
profile.assignedAssets.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 p-3 text-sm">
<div className="font-medium text-zinc-900">
{item.assetNo} · {item.name}
</div>
<div className="mt-1 text-zinc-500">位置:{item.location ?? "-"} · 状态:{item.status}</div>
</div>
))
) : (
<div className="text-sm text-zinc-500">当前无挂名资产</div>
)}
</div>
</div>
</div>
<div className="card-std p-6">
<h3 className="text-base font-semibold text-zinc-900">Checklist</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>场景</TableHead>
<TableHead>任务</TableHead>
<TableHead>状态</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{profile.lifecycle.checklistTasks.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.scene}</TableCell>
<TableCell>{item.title}</TableCell>
<TableCell>
<StatusBadge status={item.status} />
</TableCell>
<TableCell>
<div className="flex gap-2">
<Button
size="sm"
type="button"
disabled={taskMutation.isPending || item.status !== "pending"}
onClick={() => taskMutation.mutate({ taskId: item.id, action: "complete" })}
>
完成
</Button>
<Button
size="sm"
variant="secondary"
type="button"
disabled={taskMutation.isPending || item.status !== "pending"}
onClick={() => taskMutation.mutate({ taskId: item.id, action: "skip" })}
>
跳过
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
{canManageEmployeeProfile ? (
<TabsContent value="contracts" className="space-y-6">
{contractsQuery.isError ? (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
{errorMessage(contractsQuery.error, "加载合同失败")}
</div>
) : null}
<div className="grid gap-6 xl:grid-cols-[1.2fr,0.8fr]">
<div className="card-std p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">合同列表</h3>
<div className="mt-1 text-sm text-zinc-500">状态由终止日期与结束日期自动推导,本轮仅支持新增与编辑。</div>
</div>
<div className="text-sm text-zinc-500">{contracts.length} 份合同</div>
</div>
<div className="mt-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>合同编号</TableHead>
<TableHead>类型</TableHead>
<TableHead>状态</TableHead>
<TableHead>日期</TableHead>
<TableHead>附件路径</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{contracts.length ? (
contracts.map((contract) => (
<TableRow key={contract.id}>
<TableCell>{contract.contractNo}</TableCell>
<TableCell>{contractTypeLabel(contract.contractType)}</TableCell>
<TableCell>
<StatusBadge status={contract.status} />
</TableCell>
<TableCell>
<div>{contract.startDate ?? "-"} ~ {contract.endDate ?? "-"}</div>
<div className="text-xs text-zinc-500">终止:{contract.terminatedDate ?? "-"}</div>
</TableCell>
<TableCell className="max-w-[220px] truncate">{contract.attachmentPath ?? "-"}</TableCell>
<TableCell>
<Button type="button" size="sm" variant="secondary" onClick={() => startEditingContract(contract)}>
编辑
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell className="py-3 text-zinc-500" colSpan={6}>
{contractsQuery.isLoading ? "加载合同中..." : "暂无合同记录"}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
<div className="card-std space-y-4 p-6">
<div>
<h3 className="text-base font-semibold text-zinc-900">{editingContractId ? "编辑合同" : "新增合同"}</h3>
<div className="mt-1 text-sm text-zinc-500">附件本轮只保存路径字符串,不做上传。</div>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-1">
<div className="grid gap-2">
<label className="text-sm text-zinc-500">合同编号</label>
<Input value={contractForm.contractNo} onChange={(event) => setContractForm((prev) => ({ ...prev, contractNo: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">合同类型</label>
<select
className="select-std"
value={contractForm.contractType}
onChange={(event) => setContractForm((prev) => ({ ...prev, contractType: event.target.value as EmployeeContractType }))}
>
{CONTRACT_TYPE_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">开始日期</label>
<Input type="date" value={contractForm.startDate} onChange={(event) => setContractForm((prev) => ({ ...prev, startDate: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">结束日期</label>
<Input type="date" value={contractForm.endDate} onChange={(event) => setContractForm((prev) => ({ ...prev, endDate: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">终止日期</label>
<Input type="date" value={contractForm.terminatedDate} onChange={(event) => setContractForm((prev) => ({ ...prev, terminatedDate: event.target.value }))} />
</div>
<div className="grid gap-2">
<label className="text-sm text-zinc-500">附件路径</label>
<Input value={contractForm.attachmentPath} onChange={(event) => setContractForm((prev) => ({ ...prev, attachmentPath: event.target.value }))} placeholder="例如:contracts/labor/E1001.pdf" />
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button type="button" onClick={() => contractMutation.mutate()} disabled={contractMutation.isPending}>
{contractMutation.isPending ? "保存中..." : editingContractId ? "保存修改" : "新增合同"}
</Button>
{editingContractId ? (
<Button type="button" variant="secondary" onClick={resetContractEditor}>
取消编辑
</Button>
) : null}
</div>
</div>
</div>
</TabsContent>
) : null}
</Tabs>
</div>
);
}
export default EmployeeProfilePage;