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
bcdd0f4c
Commit
bcdd0f4c
authored
Oct 13, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(emr): display clinic dates in Shanghai timezone
parent
1211c4ab
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 @
bcdd0f4c
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 @
bcdd0f4c
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 @
bcdd0f4c
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 @
bcdd0f4c
...
...
@@ -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 @
bcdd0f4c
# 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