Skip to content

Commit bb7dce5

Browse files
committed
EHDS Changes step 3
1 parent 279a08b commit bb7dce5

8 files changed

Lines changed: 556 additions & 11 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"watch": "ng build --watch --configuration development",
1010
"test": "ng test",
1111
"download-ucum": "node scripts/download-ucum-units.js",
12-
"download-ehdsi-results": "node scripts/download-ehdsi-results-coded-value.js"
12+
"download-ehdsi-results": "node scripts/download-ehdsi-results-coded-value.js",
13+
"download-ehdsi-lab-technique": "node scripts/download-ehdsi-lab-technique.js"
1314
},
1415
"private": true,
1516
"dependencies": {
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
const https = require('https');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const VALUESET_URL = 'https://fhir.ehdsi.eu/laboratory/ValueSet-eHDSILabTechniqueWithExceptions.html';
6+
const OUTPUT_FILE = path.join(__dirname, '../src/assets/data/ehdsi-lab-technique-with-exceptions.json');
7+
8+
/**
9+
* Download HTML content from URL
10+
*/
11+
function downloadHTML(url) {
12+
return new Promise((resolve, reject) => {
13+
https.get(url, (res) => {
14+
let data = '';
15+
16+
res.on('data', (chunk) => {
17+
data += chunk;
18+
});
19+
20+
res.on('end', () => {
21+
resolve(data);
22+
});
23+
}).on('error', (err) => {
24+
reject(err);
25+
});
26+
});
27+
}
28+
29+
/**
30+
* Parse HTML and extract codes from the expansion table
31+
*/
32+
function parseValueSetCodes(html) {
33+
const codes = [];
34+
35+
// Extract table rows - look for the expansion table
36+
const tableRegex = /<table[^>]*>([\s\S]*?)<\/table>/gi;
37+
const tables = html.match(tableRegex) || [];
38+
39+
for (const table of tables) {
40+
// Check if this table contains codes (has "snomed.info" or "terminology.hl7.org" in it)
41+
if (!table.includes('snomed.info') && !table.includes('terminology.hl7.org')) {
42+
continue;
43+
}
44+
45+
// Extract rows
46+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
47+
let match;
48+
let isHeader = true;
49+
50+
while ((match = rowRegex.exec(table)) !== null) {
51+
const row = match[1];
52+
53+
// Skip header row
54+
if (isHeader) {
55+
isHeader = false;
56+
continue;
57+
}
58+
59+
// Extract cells
60+
const cellRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
61+
const cells = [];
62+
let cellMatch;
63+
64+
while ((cellMatch = cellRegex.exec(row)) !== null) {
65+
// Remove HTML tags and decode entities
66+
let cellContent = cellMatch[1]
67+
.replace(/<[^>]+>/g, '')
68+
.replace(/&nbsp;/g, ' ')
69+
.replace(/&amp;/g, '&')
70+
.replace(/&lt;/g, '<')
71+
.replace(/&gt;/g, '>')
72+
.replace(/&quot;/g, '"')
73+
.replace(/&apos;/g, "'")
74+
.replace(/\s+/g, ' ')
75+
.trim();
76+
77+
cells.push(cellContent);
78+
}
79+
80+
// Expected format: [Code, System, Display, Definition?]
81+
if (cells.length >= 3) {
82+
const code = cells[0].trim();
83+
const system = cells[1].trim();
84+
let display = cells[2].trim();
85+
86+
// Simplify NullFlavor displays
87+
if (system === 'http://terminology.hl7.org/CodeSystem/v3-NullFlavor') {
88+
if (code === 'OTH') {
89+
display = 'Other';
90+
} else if (code === 'UNC') {
91+
display = 'Unencoded';
92+
} else if (code === 'UNK') {
93+
display = 'Unknown';
94+
}
95+
}
96+
97+
// Only process if we have valid code, system, and display
98+
if (code && system && display) {
99+
codes.push({
100+
code: code,
101+
display: display,
102+
system: system
103+
});
104+
}
105+
}
106+
}
107+
108+
// If we found codes in this table, break (we only need the expansion table)
109+
if (codes.length > 0) {
110+
break;
111+
}
112+
}
113+
114+
return codes;
115+
}
116+
117+
/**
118+
* Main function
119+
*/
120+
async function main() {
121+
try {
122+
console.log('Downloading eHDSI Laboratory Technique with exceptions from:', VALUESET_URL);
123+
const html = await downloadHTML(VALUESET_URL);
124+
125+
console.log('Parsing HTML...');
126+
const codes = parseValueSetCodes(html);
127+
128+
if (codes.length === 0) {
129+
throw new Error('No codes found in HTML. The page structure may have changed.');
130+
}
131+
132+
console.log(`Found ${codes.length} codes`);
133+
134+
// Sort by display name for better UX
135+
codes.sort((a, b) => a.display.localeCompare(b.display));
136+
137+
// Create output structure similar to FHIR ValueSet expansion
138+
const output = {
139+
resourceType: 'ValueSet',
140+
url: 'http://fhir.ehdsi.eu/laboratory/ValueSet/eHDSILabTechniqueWithExceptions',
141+
name: 'EHDSILabTechniqueWithExceptions',
142+
title: 'eHDSI Laboratory Technique with exceptions',
143+
status: 'draft',
144+
experimental: true,
145+
date: new Date().toISOString(),
146+
description: 'The Value Set is used to code laboratory techniques for result measurements and includes exceptional values. It is defined as the union of: (a) eHDSI Laboratory Technique (b) eHDSI Exceptional Value',
147+
expansion: {
148+
timestamp: new Date().toISOString(),
149+
contains: codes.map(code => ({
150+
system: code.system,
151+
code: code.code,
152+
display: code.display
153+
}))
154+
}
155+
};
156+
157+
// Ensure output directory exists
158+
const outputDir = path.dirname(OUTPUT_FILE);
159+
if (!fs.existsSync(outputDir)) {
160+
fs.mkdirSync(outputDir, { recursive: true });
161+
}
162+
163+
// Write to file
164+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(output, null, 2), 'utf8');
165+
166+
console.log(`✅ Successfully saved ${codes.length} codes to: ${OUTPUT_FILE}`);
167+
console.log(` Codes:`, codes.map(c => `${c.code} - ${c.display}`).join(', '));
168+
169+
} catch (error) {
170+
console.error('❌ Error:', error.message);
171+
process.exit(1);
172+
}
173+
}
174+
175+
// Run the script
176+
main();

src/app/ehds-laboratory-demo/diagnostic-report-form/diagnostic-report-form.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ <h3 class="section-title">
932932

933933
@if (specimens.length > 0) {
934934
<div class="reference-list">
935-
@for (specimen of specimens; track specimen; let i = $index) {
935+
@for (specimen of specimens; track $index; let i = $index) {
936936
<div class="reference-item-display">
937937
<div class="reference-info">
938938
<mat-icon>science</mat-icon>
@@ -995,7 +995,7 @@ <h3 class="section-title">
995995

996996
@if (results.length > 0) {
997997
<div class="reference-list">
998-
@for (result of results; track result; let i = $index) {
998+
@for (result of results; track $index; let i = $index) {
999999
<div class="reference-item-display">
10001000
<div class="reference-info">
10011001
<mat-icon>bar_chart</mat-icon>
@@ -1210,7 +1210,7 @@ <h4>DiagnosticReport.conclusionCode</h4>
12101210

12111211
@if (conclusionCodes.length > 0) {
12121212
<div class="reference-list">
1213-
@for (code of conclusionCodes; track code; let i = $index) {
1213+
@for (code of conclusionCodes; track $index; let i = $index) {
12141214
<div class="reference-item-display">
12151215
<div class="reference-info">
12161216
<mat-icon>medical_services</mat-icon>

src/app/ehds-laboratory-demo/observation-result-form/observation-result-form.component.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@
110110
grid-column: 1 / -1;
111111
}
112112

113+
.full-width-field {
114+
width: 100%;
115+
}
116+
113117
/* Alignment for form fields */
114118
.form-grid {
115119
align-items: start;

src/app/ehds-laboratory-demo/observation-result-form/observation-result-form.component.html

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ <h4>Observation.valueQuantity.unit</h4>
295295
<mat-autocomplete
296296
#unitAuto="matAutocomplete"
297297
[displayWith]="displayUnitFn"
298-
(optionSelected)="onUnitSelected($event)">
298+
(optionSelected)="onUnitSelected($event)"
299+
(closed)="onUnitAutocompleteClosed()">
299300
@if (unitOptionsLoading) {
300301
<mat-option disabled>
301302
<mat-spinner diameter="35"></mat-spinner>
@@ -518,6 +519,83 @@ <h4>Observation.referenceRange</h4>
518519
</div>
519520

520521
<div class="fhir-field-wrapper">
522+
<div class="fhir-badges-container">
523+
<button type="button" mat-button class="fhir-badge" [matMenuTriggerFor]="methodMenu">
524+
FHIR
525+
</button>
526+
<button type="button" mat-button class="fhir-badge valueset-badge" (click)="openValuesetDialog('https://fhir.ehdsi.eu/laboratory/ValueSet-eHDSILabTechniqueWithExceptions.html', 'Method', 'eHDSI Laboratory Technique with exceptions')">
527+
BINDING
528+
</button>
529+
</div>
530+
<mat-menu #methodMenu="matMenu" class="fhir-info-menu" [overlapTrigger]="false" [panelClass]="'fhir-menu-panel'">
531+
<div class="fhir-menu-content">
532+
<div class="fhir-menu-header">
533+
<h4>Observation.method</h4>
534+
<a href="https://fhir.ehdsi.eu/laboratory/StructureDefinition-Observation-resultslab-lab-myhealtheu-definitions.html#Observation.method" target="_blank" rel="noopener noreferrer" class="fhir-menu-link">
535+
<mat-icon>link</mat-icon>
536+
</a>
537+
</div>
538+
<div class="fhir-menu-body">
539+
<div class="fhir-menu-item">
540+
<span class="fhir-label">Definition:</span>
541+
<span class="fhir-value">Indicates the mechanism used to perform the observation.</span>
542+
</div>
543+
<div class="fhir-menu-item">
544+
<span class="fhir-label">Control:</span>
545+
<span class="fhir-value">0..1</span>
546+
</div>
547+
<div class="fhir-menu-item">
548+
<span class="fhir-label">Type:</span>
549+
<span class="fhir-value">CodeableConcept</span>
550+
</div>
551+
<div class="fhir-menu-item">
552+
<span class="fhir-label">Binding:</span>
553+
<span class="fhir-value">The codes SHALL be taken from eHDSI Laboratory Technique with exceptions (required to <a href="https://fhir.ehdsi.eu/laboratory/ValueSet-eHDSILabTechniqueWithExceptions.html" target="_blank" rel="noopener noreferrer" class="valueset-link">http://fhir.ehdsi.eu/laboratory/ValueSet/eHDSILabTechniqueWithExceptions</a>).</span>
554+
</div>
555+
<div class="fhir-menu-item">
556+
<span class="fhir-label">Comments:</span>
557+
<span class="fhir-value">Laboratory technique (method of measurement) are integral parts of the test specification. Only used if not implicit in code for Observation.code.</span>
558+
</div>
559+
</div>
560+
</div>
561+
</mat-menu>
562+
<mat-form-field>
563+
<mat-label>Method</mat-label>
564+
@if (methodOptionsLoading) {
565+
<mat-spinner matSuffix diameter="20"></mat-spinner>
566+
}
567+
<input
568+
matInput
569+
[formControl]="methodInputControl"
570+
[matAutocomplete]="methodAuto"
571+
#methodAutoTrigger="matAutocompleteTrigger"
572+
[readonly]="isViewOnly"
573+
(focus)="onMethodInputFocus()"
574+
placeholder="Search laboratory technique...">
575+
<mat-autocomplete
576+
#methodAuto="matAutocomplete"
577+
[displayWith]="displayMethodFn"
578+
(optionSelected)="onMethodSelected($event)"
579+
(closed)="onMethodAutocompleteClosed()">
580+
@if (methodOptionsLoading) {
581+
<mat-option disabled>
582+
<mat-spinner diameter="35"></mat-spinner>
583+
Loading techniques...
584+
</mat-option>
585+
} @else {
586+
@for (option of methodFilteredOptions | async; track option.code) {
587+
<mat-option [value]="option">
588+
{{ option.display }}@if (option.display !== option.code) {
589+
<span class="unit-code-hint"> ({{ option.code }})</span>
590+
}
591+
</mat-option>
592+
}
593+
}
594+
</mat-autocomplete>
595+
</mat-form-field>
596+
</div>
597+
598+
<div class="fhir-field-wrapper full-width">
521599
<div class="fhir-badges-container">
522600
<button type="button" mat-button class="fhir-badge" [matMenuTriggerFor]="noteMenu">
523601
FHIR
@@ -543,7 +621,7 @@ <h4>Observation.note</h4>
543621
</div>
544622
</div>
545623
</mat-menu>
546-
<mat-form-field>
624+
<mat-form-field class="full-width-field">
547625
<mat-label>Note</mat-label>
548626
<input matInput formControlName="note" [readonly]="isViewOnly" placeholder="Additional notes about the observation">
549627
</mat-form-field>

0 commit comments

Comments
 (0)