Commit 45096f85 by luoqi

feat(web): 召回池已认领患者加「返池」按钮(回收→退回召回池)

已认领(status=assigned)的患者行,PLAN_RECYCLE 权限(leader/admin)下 hover 出「返池」按钮:
调 plansApi.recycle → 后端置 active + 清 assignee → 原地 patchItem 改回「待认领」(不重拉)。
与「认领」共用同一 grid 叠层 hover-swap 结构(不重排);amber 区别 teal。本地 dev 实测:
点返池 → 进行中变待认领+现认领按钮、toast「已返池」、DB status=active/assignee=null,链路通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 1bbf0774
Pipeline #3382 failed in 0 seconds
...@@ -49,6 +49,7 @@ export function PatientPickerRail({ ...@@ -49,6 +49,7 @@ export function PatientPickerRail({
}) { }) {
const router = useRouter(); const router = useRouter();
const canViewAll = useHasPermission(Permission.PLAN_VIEW_ALL); const canViewAll = useHasPermission(Permission.PLAN_VIEW_ALL);
const canRecycle = useHasPermission(Permission.PLAN_RECYCLE); // 「返池」按钮门控(leader/admin)
// 默认进「我的」;加载后若「我的」为空,自动回落「召回池」(见下方 effect)。 // 默认进「我的」;加载后若「我的」为空,自动回落「召回池」(见下方 effect)。
// 与落地页选默认患者同策略(mine 优先,空则 pool)。 // 与落地页选默认患者同策略(mine 优先,空则 pool)。
...@@ -139,6 +140,18 @@ export function PatientPickerRail({ ...@@ -139,6 +140,18 @@ export function PatientPickerRail({
} }
}; };
// 返池:回收已认领的 plan → 原地把行改回"待认领"、清 assignee(不重拉,保滚动/筛选)。
// plan 仍 active,回到召回池可被再次认领。需 PLAN_RECYCLE 权限。
const recycle = async (planId: string, name: string) => {
try {
await plansApi.recycle(planId);
patchItem(planId, { status: 'active', assigneeUserId: null });
toast.success(`已返池 ${name}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : '返池失败');
}
};
// 触底加载 // 触底加载
const listRef = useRef<HTMLDivElement>(null); const listRef = useRef<HTMLDivElement>(null);
const onScroll = () => { const onScroll = () => {
...@@ -271,6 +284,11 @@ export function PatientPickerRail({ ...@@ -271,6 +284,11 @@ export function PatientPickerRail({
? () => void claim(p.id, p.patient.name ?? '') ? () => void claim(p.id, p.patient.name ?? '')
: undefined : undefined
} }
onRecycle={
canRecycle && p.status === 'assigned'
? () => void recycle(p.id, p.patient.name ?? '')
: undefined
}
/> />
))} ))}
{loading && ( {loading && (
...@@ -496,6 +514,7 @@ function PatientRow({ ...@@ -496,6 +514,7 @@ function PatientRow({
hidePhone, hidePhone,
onClick, onClick,
onClaim, onClaim,
onRecycle,
}: { }: {
plan: PlanListItem; plan: PlanListItem;
active: boolean; active: boolean;
...@@ -504,6 +523,8 @@ function PatientRow({ ...@@ -504,6 +523,8 @@ function PatientRow({
hidePhone: boolean; hidePhone: boolean;
onClick: () => void; onClick: () => void;
onClaim?: () => void; onClaim?: () => void;
/** 已认领 plan 的「返池」回收动作(仅 PLAN_RECYCLE 权限 + status=assigned 时传入) */
onRecycle?: () => void;
}) { }) {
const breakdown = (p.reasons[0]?.breakdown as { priority?: PriorityBreakdown } | null | undefined)?.priority; const breakdown = (p.reasons[0]?.breakdown as { priority?: PriorityBreakdown } | null | undefined)?.priority;
return ( return (
...@@ -551,9 +572,16 @@ function PatientRow({ ...@@ -551,9 +572,16 @@ function PatientRow({
</span> </span>
)} )}
<span className="ml-auto flex-none" /> <span className="ml-auto flex-none" />
{onClaim ? ( {(() => {
// 药丸与「认领」按钮叠在同一 grid 格:槽位宽/高恒为两者较大值,hover 只切 visibility // 动作按钮:待认领→「认领」(teal),已认领→「返池」(amber)。二者与状态药丸叠同 grid 格,
// (非 display),不改变布局 → 悬停时整行不重排、不位移。 // 槽位宽/高恒为较大值,hover 只切 visibility(非 display)→ 悬停整行不重排、不位移。
const act = onClaim
? { label: '认领', tone: 'bg-teal-600 hover:bg-teal-700', fn: onClaim }
: onRecycle
? { label: '返池', tone: 'bg-amber-500 hover:bg-amber-600', fn: onRecycle }
: null;
if (!act) return <StatusPill status={p.status} />;
return (
<span className="grid flex-none items-center justify-items-end"> <span className="grid flex-none items-center justify-items-end">
<span className="col-start-1 row-start-1 group-hover:invisible"> <span className="col-start-1 row-start-1 group-hover:invisible">
<StatusPill status={p.status} /> <StatusPill status={p.status} />
...@@ -562,16 +590,18 @@ function PatientRow({ ...@@ -562,16 +590,18 @@ function PatientRow({
type="button" type="button"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onClaim(); act.fn();
}} }}
className="invisible col-start-1 row-start-1 whitespace-nowrap rounded-md bg-teal-600 px-2 py-0.5 text-[10.5px] font-medium text-white hover:bg-teal-700 group-hover:visible" className={cn(
'invisible col-start-1 row-start-1 whitespace-nowrap rounded-md px-2 py-0.5 text-[10.5px] font-medium text-white group-hover:visible',
act.tone,
)}
> >
认领 {act.label}
</button> </button>
</span> </span>
) : ( );
<StatusPill status={p.status} /> })()}
)}
</div> </div>
</div> </div>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment