收敛企微主办 HR 考勤页面(05-30-wecom-hr-boundary-pages)
这个提交包含在:
@@ -23,7 +23,7 @@ import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -206,6 +206,7 @@ export function LeaveRequestPage() {
|
||||
const canViewAll = hasLeaveViewAll || hasLeaveManage;
|
||||
const canViewTeamCalendar = hasLeaveViewAll;
|
||||
const useDepartmentCalendarScope = hasLeaveViewAll && !hasLeaveManage && Boolean(me?.departmentId);
|
||||
const canUseOaEmergencyLeave = hasLeaveManage;
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<LeaveRequestStatus | "">("");
|
||||
const [scope, setScope] = useState<"own" | "all">("own");
|
||||
@@ -255,6 +256,7 @@ export function LeaveRequestPage() {
|
||||
|
||||
const leaveTypesQuery = useQuery({
|
||||
queryKey: ["leave", "types"],
|
||||
enabled: canUseOaEmergencyLeave,
|
||||
queryFn: () => fetchLeaveTypes({ enabled: true }),
|
||||
});
|
||||
|
||||
@@ -416,7 +418,7 @@ export function LeaveRequestPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["leave", "balances"] }),
|
||||
]);
|
||||
resetForm();
|
||||
toast.success("请假草稿已创建");
|
||||
toast.success("OA 应急请假草稿已创建");
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = resolveError(error);
|
||||
@@ -443,7 +445,7 @@ export function LeaveRequestPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["leave", "balances"] }),
|
||||
]);
|
||||
resetForm();
|
||||
toast.success("请假单已保存,可继续提交");
|
||||
toast.success("OA 应急请假单已保存");
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = resolveError(error);
|
||||
@@ -460,7 +462,7 @@ export function LeaveRequestPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["leave", "calendar"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["leave", "balances"] }),
|
||||
]);
|
||||
toast.success("请假申请已提交");
|
||||
toast.success("OA 应急请假单已提交");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(resolveError(error));
|
||||
@@ -475,7 +477,7 @@ export function LeaveRequestPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["leave", "calendar"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["leave", "balances"] }),
|
||||
]);
|
||||
toast.success("请假申请已撤回");
|
||||
toast.success("OA 应急请假单已撤回");
|
||||
setWithdrawTarget(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -486,15 +488,15 @@ export function LeaveRequestPage() {
|
||||
const totalPages = Math.max(1, Math.ceil((requestsQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||
const calendarIsTruncated = (calendarRequestsQuery.data?.total ?? 0) > (calendarRequestsQuery.data?.items.length ?? 0);
|
||||
const isSavingForm = createMutation.isPending || updateMutation.isPending;
|
||||
const formTitle = editingLeaveRequest ? "编辑请假单" : "新建请假草稿";
|
||||
const formTitle = editingLeaveRequest ? "编辑 OA 应急请假单" : "OA 应急请假草稿";
|
||||
const formDescription = editingLeaveRequest
|
||||
? "修改原单内容并保留同一请假单 ID,保存后仍可继续提交。"
|
||||
: "先填写时间段与假种,再补充附件和说明。";
|
||||
? "仅用于管理员确认企业微信不可用后的应急接管;保存后仍需按应急审批留痕。"
|
||||
: "日常请假在企业微信发起。这里只保留 HR/管理员应急接管时的 OA 草稿入口。";
|
||||
const currentAttachmentLabel =
|
||||
attachmentFile?.name ?? (editingLeaveRequest?.attachment?.originalName ?? null);
|
||||
|
||||
const leaveRecordsPanel = (
|
||||
<SectionPanel title="请假记录" description="支持按状态和范围收敛列表,草稿与驳回单可再次提交。">
|
||||
<SectionPanel title="请假记录" description="查看企业微信主办或历史 OA 留存的请假状态;日常提交仍从企业微信进入。">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="text-sm text-zinc-500">最近一次筛选会保留在当前页面状态中。</div>
|
||||
@@ -560,9 +562,9 @@ export function LeaveRequestPage() {
|
||||
{(requestsQuery.data?.items ?? []).length ? (
|
||||
requestsQuery.data?.items.map((item) => {
|
||||
const isOwner = item.ownerId === me?.id;
|
||||
const canEdit = isOwner && ["draft", "rejected"].includes(item.status);
|
||||
const canSubmit = isOwner && ["draft", "rejected"].includes(item.status);
|
||||
const canWithdraw = isOwner && item.status === "reviewing";
|
||||
const canEdit = canUseOaEmergencyLeave && isOwner && ["draft", "rejected"].includes(item.status);
|
||||
const canSubmit = canUseOaEmergencyLeave && isOwner && ["draft", "rejected"].includes(item.status);
|
||||
const canWithdraw = canUseOaEmergencyLeave && isOwner && item.status === "reviewing";
|
||||
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
@@ -596,39 +598,43 @@ export function LeaveRequestPage() {
|
||||
</TableCell>
|
||||
<TableCell>{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={!canEdit || isSavingForm}
|
||||
onClick={() => startEditing(item)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
disabled={!canSubmit || submitMutation.isPending}
|
||||
onClick={() => submitMutation.mutate(item.id)}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={!canWithdraw || withdrawMutation.isPending}
|
||||
onClick={() =>
|
||||
setWithdrawTarget({
|
||||
id: item.id,
|
||||
label: `${item.leaveType?.name ?? "请假"} · ${formatRangeLabel(item)}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
撤回
|
||||
</Button>
|
||||
</div>
|
||||
{canUseOaEmergencyLeave ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={!canEdit || isSavingForm}
|
||||
onClick={() => startEditing(item)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
disabled={!canSubmit || submitMutation.isPending}
|
||||
onClick={() => submitMutation.mutate(item.id)}
|
||||
>
|
||||
提交应急审批
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={!canWithdraw || withdrawMutation.isPending}
|
||||
onClick={() =>
|
||||
setWithdrawTarget({
|
||||
id: item.id,
|
||||
label: `${item.leaveType?.name ?? "请假"} · ${formatRangeLabel(item)}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
撤回
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-zinc-400">企业微信处理</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -780,23 +786,31 @@ export function LeaveRequestPage() {
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="请假申请"
|
||||
subtitle="支持假种选择、半天粒度请假、额度冻结与撤回释放。"
|
||||
title="请假记录"
|
||||
subtitle="企业微信主办请假、补卡、外出和加班;OA 只做记录、额度摘要、团队排班参考和应急留痕。"
|
||||
/>
|
||||
<WorkspaceNotice title="企业微信主办" tone="info">
|
||||
日常请假请在企业微信完成。OA 不重复发起常规请假,只展示同步/历史状态;只有 HR 或管理员确认应急接管时才使用 OA 应急草稿。
|
||||
</WorkspaceNotice>
|
||||
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard title="可用额度" value={balanceSummary.available.toFixed(1)} note="按当前启用假种汇总" />
|
||||
<MetricCard title="已冻结" value={balanceSummary.reserved.toFixed(1)} note="审批中申请会占用额度" tone={balanceSummary.reserved > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="已使用" value={balanceSummary.used.toFixed(1)} note="通过后计入已使用" />
|
||||
<MetricCard
|
||||
title="本次预估"
|
||||
value={effectiveRequestedDaysPreview ?? "-"}
|
||||
note="创建草稿前可先预览天数"
|
||||
tone={effectiveRequestedDaysPreview ? "warning" : "neutral"}
|
||||
/>
|
||||
{canUseOaEmergencyLeave ? (
|
||||
<MetricCard
|
||||
title="应急预估"
|
||||
value={effectiveRequestedDaysPreview ?? "-"}
|
||||
note="仅 OA 应急草稿使用"
|
||||
tone={effectiveRequestedDaysPreview ? "warning" : "neutral"}
|
||||
/>
|
||||
) : (
|
||||
<MetricCard title="主办系统" value="企微" note="OA 只读留存和异常说明" tone="neutral" />
|
||||
)}
|
||||
</StatStrip>
|
||||
|
||||
<div className="page-shell-grid">
|
||||
{canUseOaEmergencyLeave ? (
|
||||
<SectionPanel title={formTitle} description={formDescription}>
|
||||
<div className="space-y-4">
|
||||
{formError ? (
|
||||
@@ -813,7 +827,7 @@ export function LeaveRequestPage() {
|
||||
|
||||
{isNonWorkingPreview ? (
|
||||
<p className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-700">
|
||||
所选日期范围均为非工作日,无需提交请假
|
||||
所选日期范围均为非工作日,无需发起请假
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -957,14 +971,21 @@ export function LeaveRequestPage() {
|
||||
? updateMutation.isPending
|
||||
? "保存中..."
|
||||
: "保存修改"
|
||||
: createMutation.isPending
|
||||
? "创建中..."
|
||||
: "创建草稿"}
|
||||
: createMutation.isPending
|
||||
? "创建中..."
|
||||
: "创建应急草稿"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
) : (
|
||||
<SectionPanel title="日常请假入口" description="普通员工不在 OA 里重复提交请假。">
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm leading-6 text-emerald-800">
|
||||
请在企业微信完成请假、补卡、外出和加班。OA 当前用于查看记录、额度摘要、团队排班和同步异常;如企业微信不可用,由 HR 或管理员开启应急接管后再使用 OA。
|
||||
</div>
|
||||
</SectionPanel>
|
||||
)}
|
||||
|
||||
<SectionPanel title="额度概览" description="假种可用额度、冻结与已使用情况按自然周期汇总。">
|
||||
<div className="space-y-4">
|
||||
@@ -1050,7 +1071,7 @@ export function LeaveRequestPage() {
|
||||
setWithdrawTarget(null);
|
||||
}
|
||||
}}
|
||||
title="确认撤回请假申请?"
|
||||
title="确认撤回 OA 应急请假单?"
|
||||
description={withdrawTarget ? `将撤回:${withdrawTarget.label}` : undefined}
|
||||
confirmLabel="确认撤回"
|
||||
onConfirm={async () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户