Commit 0ffe757b by luoqi

feat(主管工作台): 助手确认/撤销后,两张表自己刷新

产品:确认分配后主管工作台的两个表格能不能做到刷新。

确认单在**助手浮层**里、两张表在页面上,两者没有父子关系 —— 谁也拿不到谁的 setState。
所以走一个极简全局信号(与 plan-sync-store「详情页动作同步左栏」同一套路):
  确认分配 / 撤销这批 → notifyChanged()
  两张表的 fetch effect → 把 seq 放进依赖,重新拉

 **只发 seq,不带任何数据**( 不学 plan-sync 那样带 planId/status):
   那边改的是"某一行的角标",能就地打补丁;这里落库的是**一整批** ——
   批次列表多一行、每个客服的在手/分到全变、退回率的分母也变。
   想靠事件把这些补丁算准,等于把服务端的聚合口径在前端再实现一遍
   (必漂,而且不报错)。⇒ 一律重拉,让服务端说了算(T14:口径只有一份)。

🔴 批次表重拉必须**回到第一页**(setItems([]) + setCursor(null) 一起做):
   只调 load() 会把新的一页**追加**在旧列表后面,而新批次排最前 ——
   它会出现在列表中段,这是游标分页的天然后果。
️ 发信号用 getState() 不订阅:确认单只发不收,订阅了会因 seq 自增把卡片自己也重渲染。

实测(全程没刷页面):
  确认前            全部 27 · 在手 35
  确认「早矫 2 条」  全部 28 · 在手 37
  点「撤销这批」     全部 28 · 在手 35   ← 批次仍在(标已撤销,列表保留做回顾),在手正确退回

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent a50ad3ff
Pipeline #3568 failed in 0 seconds
......@@ -16,6 +16,7 @@ import {
} from '@pac/types';
import { Can } from '@/components/can';
import { assignmentsApi } from '@/components/plans/assignments-api';
import { useAssignmentSyncStore } from '@/stores/assignment-sync-store';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
......@@ -323,6 +324,8 @@ export function AssignmentConfirmSheet({
const res = await assignmentsApi.revoke(assignmentId);
setRevoked(res);
setConfirmRevoke(false);
// ⭐ 撤销同样要让工作台重拉 —— 那一批的条数、每个人的在手都变了(见 assignment-sync-store)
useAssignmentSyncStore.getState().notifyChanged();
// ⭐ 结果**原话**进对话:note 是服务端拼的成品句子(含"已看过的没收回"那半句),
// 既给主管看,也补进模型上下文 —— 否则它下一轮会说"整批都撤回了"
onEditApplied?.(res.note);
......@@ -529,6 +532,13 @@ export function AssignmentConfirmSheet({
(dropped.size ? ` · 主管移除 ${dropped.size} 条` : '') +
(res.skipped.length ? ` · ${res.skipped.length} 条未落(确认期间被他人认领)` : '');
setConfirmedAt(Date.now());
/**
* ⭐ 让主管工作台的两张表重新拉一次 —— 确认完他最想看的就是自己刚分的那一批。
* ⚠️ 用全局信号而不是回调:确认单在**助手浮层**里,两张表在页面上,两边没有父子关系。
* ⚠️ `getState()` 直接取,⛔ 不用 hook 订阅 —— 这里只发不收,订阅了反而会因为
* seq 自增把这张卡片也重渲染一遍。
*/
useAssignmentSyncStore.getState().notifyChanged();
// 🔴 模型那份**必须带完整 uuid**:它要撤销只能靠这里拿 id。
// 此前这句写的是 `批次 #${id.slice(0,8)}`,两边共用 —— 于是模型手里只有 8 位短号,
// 一调 revoke_assignment 就报「批次不存在」,而卡片上的撤销按钮握着完整 id 一直是好的,
......
......@@ -5,6 +5,7 @@ import { X } from 'lucide-react';
import type { AssignmentBrief, AssignmentDetailResponse } from '@pac/types';
import { RELEASE_REASON_META, type ReleaseReason } from '@pac/types';
import { assignmentsApi } from '@/components/plans/assignments-api';
import { useAssignmentSyncStore } from '@/stores/assignment-sync-store';
import { cn } from '@/lib/utils';
/** 一页拉多少 —— 设计稿是滚到底继续加载,一屏 12 行左右 */
......@@ -106,11 +107,20 @@ export function BatchTracking({ clinicId }: { clinicId: string | null }) {
}
}, []);
/**
* 首屏 + 换诊所 + **助手那边确认/撤销之后**(syncSeq)重新拉。
*
* 🔴 重拉必须**回到第一页**:`setItems([])` + `setCursor(null)` 一起做,
* ⛔ 不能只 `load()` —— 那会把新的一页**追加**到旧列表后面,
* 而新分的批次排在最前,结果是它出现在了列表中段(游标分页的天然后果)。
* ⚠️ 抽屉开着时也照刷:里面的明细自己按 openId 取,不受这里影响。
*/
const syncSeq = useAssignmentSyncStore((s) => s.seq);
useEffect(() => {
setItems([]);
setCursor(null);
void load();
}, [clinicId, load]);
}, [clinicId, load, syncSeq]);
/**
* 滚到底继续加载。
......
......@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import type { AgentWorkloadResponse } from '@pac/types';
import { assignmentsApi } from '@/components/plans/assignments-api';
import { useAssignmentSyncStore } from '@/stores/assignment-sync-store';
import { cn } from '@/lib/utils';
/// 窗口档位。⚠️ 只给两档:主管要的是"这周怎么样 / 这个月怎么样",更细他也不看
......@@ -34,6 +35,8 @@ export function TeamStatus({ clinicId }: { clinicId: string | null }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/// ⭐ 助手那边确认/撤销后跟着重拉 —— 每个人的在手/分到/退回率分母都变了(见 assignment-sync-store)
const syncSeq = useAssignmentSyncStore((s) => s.seq);
useEffect(() => {
if (!clinicId) return;
let alive = true;
......@@ -47,7 +50,7 @@ export function TeamStatus({ clinicId }: { clinicId: string | null }) {
return () => {
alive = false;
};
}, [clinicId, days]);
}, [clinicId, days, syncSeq]);
const rows = data?.agents ?? [];
const inHandTotal = rows.reduce((s, a) => s + a.inHand, 0);
......
'use client';
import { create } from 'zustand';
/**
* 批次落库后的「重新拉一次」信号 —— 助手那边确认/撤销 → 主管工作台两张表跟着刷新。
*
* ── 为什么要它(2026-08-10 产品提)────────────────────────────────
* 确认单在**助手浮层**里,两张表在**页面**上,两边没有父子关系,谁也拿不到谁的 setState。
* 在此之前确认完批次,「我分的批次」和「团队现在什么状态」都还是旧的,
* 主管得手动刷页面才看得见自己刚分的那一批 —— 而那正是他确认完最想看的东西。
*
* ⭐ 只发一个自增的 `seq`,**不带任何数据**(⛔ 不学 plan-sync 那样带 planId/status)。
* 那边是"改某一行的角标",消费方能就地 patch;这里落库的是**一整批**:
* 批次列表多一行、每个客服的在手/分到全变了、退回率的分母也变了。
* 想靠事件把这些补丁算准,等于把服务端的聚合口径在前端再实现一遍(必漂,而且不报错)。
* ⇒ 一律**重新拉**,让服务端说了算(T14:口径只有一份)。
*
* ⚠️ 消费方把 `seq` 放进 fetch effect 的依赖即可;⛔ 别在这里塞防抖/节流 ——
* 一次确认只发一次,发多了说明调用点接错了,该去修调用点。
*/
interface AssignmentSyncState {
seq: number;
/** 批次落库 / 撤销之后调它 —— 订阅方重新拉数 */
notifyChanged: () => void;
}
export const useAssignmentSyncStore = create<AssignmentSyncState>((set) => ({
seq: 0,
notifyChanged: () => set((s) => ({ seq: s.seq + 1 })),
}));
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