Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
a391019d
Commit
a391019d
authored
Oct 13, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
Merge Friday EMR display fix into test
parents
2c1e4a14
bcdd0f4c
Pipeline
#3782
failed in 0 seconds
Changes
5
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
86 additions
and
1 deletions
+86
-1
apps/pac-service/tests/emr-backfill.spec.ts
+31
-0
apps/pac-web/src/components/plan-detail/emr-date.test.ts
+16
-0
apps/pac-web/src/components/plan-detail/emr-date.ts
+13
-0
apps/pac-web/src/components/plan-detail/emr-soap-view.tsx
+2
-1
docs/qa/2026-10-13-friday-emr-display.md
+24
-0
No files found.
apps/pac-service/tests/emr-backfill.spec.ts
View file @
a391019d
import
{
join
}
from
'node:path'
;
import
{
Logger
}
from
'@nestjs/common'
;
import
{
readEmrNarrative
}
from
'@pac/types'
;
import
{
EmrNormalizationReplay
,
type
EmrEvidence
}
from
'../src/cli/backfill-emr-normalization.lib'
;
beforeAll
(()
=>
Logger
.
overrideLogger
(
false
));
...
...
@@ -19,6 +20,36 @@ const evidence: EmrEvidence = {
function
setup
()
{
return
new
EmrNormalizationReplay
(
join
(
__dirname
,
'..'
,
'data'
,
'jvs-dw'
));
}
test
.
each
([
[
'2023-11-14 06:01:00'
,
'2023-11-13T22:01:00.000Z'
,
'无不适'
,
'复诊'
,
'OHI,更换结扎圈'
],
[
'2023-12-17 09:36:00'
,
'2023-12-17T01:36:00.000Z'
,
'复查'
,
'更换结扎圈'
,
'前牙更换结扎圈,结扎丝加力'
],
])(
'Friday 旧病历恢复原文且不改变正确的源时间 %s'
,
(
sourceTime
,
utc
,
examination
,
treatment
,
disposal
)
=>
{
const
replay
=
new
EmrNormalizationReplay
(
join
(
__dirname
,
'..'
,
'data'
,
'friday'
));
const
raw
=
{
tenant_id
:
'brand-fixture'
,
emr_sub_id
:
'E1'
,
patient_id
:
'P1'
,
organization_id
:
'C1'
,
status
:
4
,
clinic_time
:
sourceTime
,
created_gmt_at
:
sourceTime
,
updated_gmt_at
:
null
,
examine
:
[{
toothPosition
:
''
,
value
:
examination
}],
treat
:
[{
toothPosition
:
''
,
value
:
treatment
,
linkCode
:
''
}],
dispose
:
[{
toothPosition
:
''
,
value
:
disposal
}],
auxi_exam
:
'辅助检查原文'
,
};
const
result
=
replay
.
replay
({
...
evidence
,
tenantId
:
'friday-market'
,
rawPayload
:
raw
},
{
sourceUnit
:
'brand-fixture'
,
patientExternalId
:
'P1'
,
});
expect
(
result
.
occurredAt
.
toISOString
()).
toBe
(
utc
);
const
old
=
{
...
result
.
content
,
exam_findings
:
JSON
.
stringify
(
raw
.
examine
),
disposal
:
JSON
.
stringify
(
raw
.
dispose
),
treatment_notes
:
null
,
aux_exam_findings
:
null
,
custom_note
:
'保留'
,
};
const
fixed
=
replay
.
patchContent
(
old
,
result
.
content
,
result
.
occurredAt
,
false
);
for
(
const
[
key
,
message
]
of
Object
.
entries
({
exam_findings
:
examination
,
disposal
,
treatment_notes
:
treatment
,
aux_exam_findings
:
'辅助检查原文'
,
}))
expect
(
readEmrNarrative
(
fixed
[
key
]).
map
((
row
)
=>
row
.
message
)).
toEqual
([
message
]);
// treat 是本次治疗;不能凭列表页标签制造未来计划或复制处置。
expect
(
fixed
.
treatment_plan
??
null
).
toBeNull
();
expect
(
fixed
.
custom_note
).
toBe
(
'保留'
);
expect
(
replay
.
patchContent
(
fixed
,
result
.
content
,
result
.
occurredAt
,
false
)).
toEqual
(
fixed
);
});
test
(
'存量重放从源时间纠正 UTC,身份和其他内容保持,重复修复内容稳定'
,
()
=>
{
const
replay
=
setup
();
const
result
=
replay
.
replay
(
evidence
,
{
sourceUnit
:
'瑞尔'
,
patientExternalId
:
'P1'
});
...
...
apps/pac-web/src/components/plan-detail/emr-date.test.ts
0 → 100644
View file @
a391019d
import
{
describe
,
expect
,
it
}
from
'vitest'
;
import
{
formatEmrDate
}
from
'./emr-date'
;
describe
(
'病历就诊日期'
,
()
=>
{
it
.
each
([
[
'2023-11-13T22:01:00.000Z'
,
'2023-11-14'
],
[
'2023-12-17T01:36:00.000Z'
,
'2023-12-17'
],
[
'2023-12-31T16:00:00.000Z'
,
'2024-01-01'
],
[
'2023-11-14T06:01:00+08:00'
,
'2023-11-14'
],
[
'2025-03-03T02:34:29.000Z'
,
'2025-03-03'
],
[
'2023-11-14'
,
'2023-11-14'
],
[
null
,
'—'
],
[
undefined
,
'—'
],
[
''
,
'—'
],
[
'invalid'
,
'—'
],
])(
'%s → %s'
,
(
value
,
expected
)
=>
{
expect
(
formatEmrDate
(
value
)).
toBe
(
expected
);
});
});
apps/pac-web/src/components/plan-detail/emr-date.ts
0 → 100644
View file @
a391019d
const
clinicDate
=
new
Intl
.
DateTimeFormat
(
'en-CA'
,
{
timeZone
:
'Asia/Shanghai'
,
year
:
'numeric'
,
month
:
'2-digit'
,
day
:
'2-digit'
,
});
/** 当前宿主均使用中国诊所日历日;不能截取入库 UTC 日期,也不能依赖浏览器时区。 */
export
function
formatEmrDate
(
value
:
string
|
null
|
undefined
):
string
{
if
(
!
value
)
return
'—'
;
if
(
/^
\d{4}
-
\d{2}
-
\d{2}
$/
.
test
(
value
))
return
value
;
const
date
=
new
Date
(
value
);
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
'—'
;
const
parts
=
clinicDate
.
formatToParts
(
date
);
return
[
'year'
,
'month'
,
'day'
].
map
((
key
)
=>
parts
.
find
((
part
)
=>
part
.
type
===
key
)
!
.
value
).
join
(
'-'
);
}
apps/pac-web/src/components/plan-detail/emr-soap-view.tsx
View file @
a391019d
...
...
@@ -5,6 +5,7 @@ import { diagnosisCodeNameZh, treatmentCategoryNameZh, readEmrNarrative } from '
import
type
{
EmrNarrativeRow
}
from
'@pac/types'
;
import
{
cn
,
formatToothPosition
}
from
'@/lib/utils'
;
import
type
{
AdaptedFact
}
from
'./adapt-data'
;
import
{
formatEmrDate
}
from
'./emr-date'
;
/**
* EmrSoapView — 病历快读(SOAP 结构化 + 多 EMR carousel 翻页)
...
...
@@ -153,7 +154,7 @@ function EmrSection({
// 所以匹配同次接诊用 emr_external_id,不能用 encounter_external_id(那个是挂号号)
const
emrId
=
String
(
c
.
emr_external_id
??
''
);
const
doctor
=
(
c
.
doctor_name
as
string
|
undefined
)
??
'—'
;
const
date
=
emr
.
occurredAt
?.
slice
(
0
,
10
)
??
'—'
;
const
date
=
formatEmrDate
(
emr
.
occurredAt
)
;
// 关联同 encounter 的 diagnosis / treatment(planned) / image
const
sameEncounter
=
(
f
:
AdaptedFact
)
=>
...
...
docs/qa/2026-10-13-friday-emr-display.md
0 → 100644
View file @
a391019d
# Friday 病历快读字段及日期核对
分支:
`codex/friday-emr-display`
,基于 main
`1211c4abdf16aca3df245f711a1ae5b246aee403`
。
## 已核实
-
测试库截图对应的两份病历,源就诊时间分别为
`2023-12-17 09:36:00`
、
`2023-11-14 06:01:00`
;入库 UTC 分别为
`2023-12-17T01:36:00Z`
、
`2023-11-13T22:01:00Z`
,转换正确。
-
病历快读使用
`occurredAt.slice(0,10)`
展示 UTC 日期,导致凌晨就诊日期少一天。本次改为明确使用 Asia/Shanghai 日历日,保持原始时间戳不变。
-
存量事实的检查和处置为含
`value`
的旧 JSON 数组,标准叙述读取器使用
`message`
,因此未展示。原始交易证据仍含完整 examine/dispose/treat。
-
main 已有 Friday 元素级规范化及 treatment_notes 保留逻辑;这些旧事实尚未执行存量修复。现有 CLI 针对该患者只读预演结果:scanned=12、changed=12、written=0、conflicted=0。
-
FridaySaaS AllMedicalRecords 列表将 treat 显示为“治疗计划”,但打印页 PrintContent、Java MedEmrInfoMvo/MedEmrInfoSaveParams 等均标为“本次治疗”。不把 dispose 重复写成本次治疗,也不将 treat 擅自改成未来治疗计划。
-
auxi_exam 是单独的辅助检查原文。现有 PAC 已在客观检查区域保留独立“辅助检查”标签;截图两条源值均为空,不应人为填充。
## 本次变更与验证
-
修复病历快读日期显示,覆盖 UTC 跨日、跨年、显式时区、纯日期及缺失/无效值。
-
增加脱敏 Friday 存量回放回归:检查、处置、本次治疗、辅助检查完整恢复;正确 UTC 时间不变;保留无关字段;重复修复内容不变。
-
Service 三组相关测试通过(32 项),包含 5i5ya 与源隔离回归。
-
Web 日期测试通过(10 项),Web 类型检查通过。
-
Service 类型检查通过;首次检查遇到本地 Prisma 生成客户端落后于当前 schema,重新生成客户端后通过,未执行数据库迁移。
## 边界
本次未提交、未部署,未修改生产;测试存量数据目前仅 dry-run。代码部署不会自动修复既有事实,后续应使用既有 backfill-emr-normalization CLI 按患者定向执行,保存私密审计文件,再次 dry-run 应 changed=0。不得使用全库重放替代这次定向修复。
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment