From 2eff8e01a2e8e9d41171c1c8d7d44f00a8639422 Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 28 Mar 2026 17:41:44 +0800 Subject: [PATCH] =?UTF-8?q?R40:=20=E5=AE=8C=E6=88=90=E6=A1=A3=E6=A1=88?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=89=A9=E5=B1=95=E4=B8=8E=E5=91=98=E5=B7=A5?= =?UTF-8?q?=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/employees.ts | 1 + client/src/api/exports.ts | 17 ++ client/src/pages/EmployeeProfilePage.tsx | 122 ++++++++++-- client/src/pages/PeoplePage.tsx | 54 ++++++ ...28_12-36-28-r37-agent-modes-governance.csv | 2 +- ...-07-r40-profile-edit-and-people-export.csv | 5 + server/src/routes/employees.js | 8 +- server/src/routes/exports.js | 179 ++++++++++++++++++ .../tests/unit/routes/employees.patch.test.js | 173 +++++++++++++++++ .../unit/routes/exports.employees.test.js | 116 ++++++++++++ 10 files changed, 654 insertions(+), 23 deletions(-) create mode 100644 issues/2026-03-28_17-12-07-r40-profile-edit-and-people-export.csv create mode 100644 server/tests/unit/routes/employees.patch.test.js create mode 100644 server/tests/unit/routes/exports.employees.test.js diff --git a/client/src/api/employees.ts b/client/src/api/employees.ts index 1921eaa7..b8c88d31 100644 --- a/client/src/api/employees.ts +++ b/client/src/api/employees.ts @@ -137,6 +137,7 @@ export async function patchEmployeeProfile( departmentId: string; directManagerId: string | null; title: string; + hireDate: string | null; probationEndDate: string | null; lastWorkingDate: string | null; workLocation: string | null; diff --git a/client/src/api/exports.ts b/client/src/api/exports.ts index 1ec5beaf..7fa298a0 100644 --- a/client/src/api/exports.ts +++ b/client/src/api/exports.ts @@ -56,4 +56,21 @@ export async function exportDashboardReimbursements(params: { return apiRequestBlob(`/api/exports/dashboard/reimbursements?${qs.toString()}`); } +export async function exportEmployees(params: { + format: "csv" | "xlsx"; + status?: "active" | "archived"; + departmentId?: string; + q?: string; + limit?: number; +}) { + const qs = new URLSearchParams(); + qs.set("format", params.format); + if (params.status) qs.set("status", params.status); + if (params.departmentId) qs.set("departmentId", params.departmentId); + if (params.q) qs.set("q", params.q); + if (typeof params.limit === "number") qs.set("limit", String(params.limit)); + + return apiRequestBlob(`/api/exports/employees?${qs.toString()}`); +} + export type { ApiBlobResponse }; diff --git a/client/src/pages/EmployeeProfilePage.tsx b/client/src/pages/EmployeeProfilePage.tsx index bd77a876..e7a567d4 100644 --- a/client/src/pages/EmployeeProfilePage.tsx +++ b/client/src/pages/EmployeeProfilePage.tsx @@ -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() { -
-
- - setForm((prev) => ({ ...prev, workLocation: event.target.value }))} /> + {canEditExtendedFields ? ( +
+
+ + setForm((prev) => ({ ...prev, title: event.target.value }))} /> +
+
+ + setForm((prev) => ({ ...prev, hireDate: event.target.value }))} /> +
+
+ + setForm((prev) => ({ ...prev, probationEndDate: event.target.value }))} /> +
+
+ + setForm((prev) => ({ ...prev, lastWorkingDate: event.target.value }))} /> +
-
- - setForm((prev) => ({ ...prev, emergencyContactName: event.target.value }))} /> -
-
- - setForm((prev) => ({ ...prev, emergencyContactPhone: event.target.value }))} /> -
-
+ ) : null} -
- -