Commit ea66930a by luoqi

fix(plan-detail): 关联客户档案跳转对齐「原始档案」—— 配了走宿主,没配回落 PAC 工单页

原来只跳 PAC 自己的工单页。宿主档案才是客服真正要看的那一页(有真手机号、有全量病历),
PAC 工单页只是兜底 —— 所以跟本人的「原始档案」同一套逻辑:

  配了 actionUrls.VIEW_PATIENT → 跳宿主档案,占位换成**这位关系人**的
                                 {patientId} / {medicalRecordNumber}
  没配 → 回落 /plans/<planId>(本 scope 内的活跃工单)
  都没有 → 只显示信息,标「无档案入口」(原来叫「无工单」,现在两条路都可能缺,措辞跟着改)

为此后端 include 补了关系人的 externalId / medicalRecordNumber —— 注意 VIEW_PATIENT 模板里的
{patientId} 是**宿主侧 id**,不是 PAC 的 uuid,拿 relatedPatientId 去填会得到一个查不到人的链接。
关系人没建档时 externalId 退回边上的 relatedExternalId(那个始终有)。

本地两条路都实测:
  未配 → /plans/2669df95…
  配了 → …/patient?pid=115199&mrn=JN0A016246 —— pid/mrn 是**关系人的**(本人是 110959/JN0A025573),
         没有串成本人的 id
708 tests / 46 suites + 两端 typecheck 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent f187fb97
Pipeline #3489 failed in 0 seconds
......@@ -60,7 +60,19 @@ export class PlanAggregateService {
profile: true,
// 联系人/亲属边 → 关系人姓名/电话从 relatedPatient 现取(patient↔patient,零冗余)
relationsOut: {
include: { relatedPatient: { select: { id: true, name: true, phone: true, birthDate: true } } },
include: {
relatedPatient: {
// externalId / 病历号:给宿主 VIEW_PATIENT 槽位的占位替换用(同本人「原始档案」那条路)
select: {
id: true,
externalId: true,
medicalRecordNumber: true,
name: true,
phone: true,
birthDate: true,
},
},
},
},
// 诊所回访记录(展示用,按时间倒序,封顶 100;每患者 p99=19,够用)
// nulls:'last' —— Postgres 的 DESC 默认 NULLS FIRST,无日期的记录会顶到列表最前,
......@@ -108,7 +120,20 @@ export class PlanAggregateService {
patient: Prisma.PatientGetPayload<{
include: {
profile: true;
relationsOut: { include: { relatedPatient: { select: { id: true; name: true; phone: true; birthDate: true } } } };
relationsOut: {
include: {
relatedPatient: {
select: {
id: true;
externalId: true;
medicalRecordNumber: true;
name: true;
phone: true;
birthDate: true;
};
};
};
};
returnVisits: true;
};
}>,
......@@ -385,6 +410,8 @@ function serializeProfile(
relatedPatientId: string | null;
relatedPatient: {
id: string;
externalId: string;
medicalRecordNumber: string | null;
name: string | null;
phone: string | null;
birthDate: Date | null;
......@@ -441,6 +468,9 @@ function serializeProfile(
isKin: KIN_RELATIONSHIPS.includes(r.relationship),
age: r.relatedPatient?.birthDate ? calcAge(r.relatedPatient.birthDate) : null,
relatedPatientId: r.relatedPatientId,
// 宿主 VIEW_PATIENT 槽位的占位值 —— 前端配了就跳宿主档案,没配才回落 PAC 工单页
externalId: r.relatedPatient?.externalId ?? r.relatedExternalId,
medicalRecordNumber: r.relatedPatient?.medicalRecordNumber ?? null,
planId: r.relatedPatientId ? (relatedPlanIdByPatient.get(r.relatedPatientId) ?? null) : null,
}))
.sort((a, b) => Number(b.linked) - Number(a.linked));
......
......@@ -65,6 +65,8 @@ export const mockPatient = {
age: 58,
relatedPatientId: 'p-mock-mother',
planId: null,
externalId: '5i5ya_PA00999001',
medicalRecordNumber: 'SH0Q099001',
},
] as Array<{
relationship: string;
......@@ -78,6 +80,9 @@ export const mockPatient = {
relatedPatientId: string | null;
/** 关系人在本 scope 内可打开的工单;null = 没有 → 只显示姓名,不给链接 */
planId: string | null;
/** 关系人宿主侧 id / 病历号 —— 填 VIEW_PATIENT 槽位占位 */
externalId: string;
medicalRecordNumber: string | null;
}>,
// ⚠️ TEST ONLY — 监护人(儿童/老人触达 fallback),真实手机号到位前仅演示
guardian: {
......
......@@ -1616,8 +1616,11 @@ function RecallReasonLine({ visibleReasons }: { visibleReasons: PlanReason[] })
// 只列**亲戚**(后端按 KIN_RELATIONSHIPS 打 isKin):friend / other 里混的是推荐人噪音,
// 把推荐人当家属去问病情比不显示更糟。
// 没建档(linked=false / 无姓名)的不出行 —— 一行只有关系没有人,客服拿不到任何可用信息。
// 「关联客户档案」跳的是 **PAC 自己的患者工单页**,新标签页开(不顶掉当前工单);
// 关系人在本 scope 内没有活跃工单(planId=null)→ 只显示信息不给链接,免得点进去 404。
// ⭐「关联客户档案」的目标**与本人的「原始档案」同一套逻辑**(业务 2026-07-30):
// 宿主配了 actionUrls.VIEW_PATIENT → 跳**宿主自己的档案页**(占位换成这位关系人的
// {patientId}/{medicalRecordNumber});没配 → 回落 PAC 自己的工单页 /plans/<planId>。
// 理由:宿主档案才是客服真正要看的那一页(有真手机号、有全量病历),PAC 工单页只是兜底。
// 两条路都新标签页开(不顶掉当前工单)。都没有(未配 + 无工单)→ 只显示信息不给链接。
// ──────────────────────────────────────────
function RelatedKinRow({ contacts }: { contacts: typeof mockPatient.profile.contacts }) {
const kin = contacts.filter((c) => c.isKin && c.linked && c.name);
......@@ -1635,24 +1638,38 @@ function RelatedKinRow({ contacts }: { contacts: typeof mockPatient.profile.cont
{c.age != null && ` · ${c.age}岁`}
</span>
<span className="ml-auto flex-none">
{c.planId ? (
{(() => {
// 宿主档案优先,PAC 工单页兜底(见组件头注释)
const hostUrl = resolveActionUrl('VIEW_PATIENT', {
patientId: c.externalId,
medicalRecordNumber: c.medicalRecordNumber,
});
const url = hostUrl ?? (c.planId ? `/plans/${c.planId}` : null);
if (!url) {
return (
<span
className="text-[10.5px] text-slate-400"
title="宿主未配置档案跳转,且该客户在你的数据范围内没有召回工单"
>
无档案入口
</span>
);
}
return (
<a
href={`/plans/${c.planId}`}
href={url}
{...HOST_LINK_PROPS}
// 走 JS 才有 sandbox 兜底(同宿主槽位那两个链接,见 action-url.ts)
onClick={(e) => {
e.preventDefault();
openHostUrl(`/plans/${c.planId}`);
openHostUrl(url);
}}
className="text-[10.5px] text-brand-700 hover:underline"
>
关联客户档案 →
</a>
) : (
<span className="text-[10.5px] text-slate-400" title="该客户在你的数据范围内没有召回工单">
无工单
</span>
)}
);
})()}
</span>
</div>
))}
......
......@@ -51,6 +51,9 @@ export type PlanDetailData = {
relatedPatientId: string | null;
/// 关系人在本 scope 内可打开的工单;null = 没有(或越权)→ 前端不给链接,免得点进去 404
planId: string | null;
/// 关系人的宿主侧 id / 病历号 —— 填 VIEW_PATIENT 槽位占位用(配了就跳宿主档案)
externalId: string;
medicalRecordNumber: string | null;
}>;
/// ⚠️ TEST ONLY — 监护人(儿童/老人触达 fallback)。真实手机号到位前仅演示用。
guardian: {
......
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