Fifteen Finnish pre-visit and screening questionnaires (esitietolomakkeet), exported from the Lunna survey tool, converted into HL7 FHIR R4 Questionnaires. A small web app fills them in, in Finnish, Swedish or English, and can pre-populate them from a patient's FHIR data.
- Converter: Lunna export → FHIR R4 Questionnaire, with translations, sections, follow-up questions (
enableWhen), scores (calculatedExpression), units, and SDC pre-population definitions. Validated with the HL7 FHIR Validator. - App: lists the questionnaires and renders them. It handles follow-ups, validation, live scores, a print view and the QuestionnaireResponse (JSON, downloadable), and checks clean with axe-core.
- Pre-population: load FHIR Bundles (FinnishCore-shaped data or IPS patient summaries; synthetic sample patients included) and fill in what the record says: measurements, diagnoses (Finnish ICD-10), medications and vaccinations (ATC), allergies, the contact person. Each filled answer shows where it came from. Only positive evidence fills an answer: a missing record never answers "no".
- Interoperability: the same Questionnaires render, score and pre-populate in LHC-Forms, and render and score in CSIRO Smart Forms (see
crosscheck/).
The plan, every decision and what was verified at each milestone are in docs/IMPLEMENTATION_PLAN.md.
Requires Node 24 or later.
npm installnpm run devThen open http://localhost:5173. Pick a questionnaire. To pre-populate, open "Potilastiedot" (patient data) on the form, choose a sample patient, e.g. Maija for the maternity clinic form (Esitietolomake - Äitiysneuvola), and press "Täytä lomake potilastiedoista" (fill in from patient data).
| Path | What it is |
|---|---|
Esitieto_lomakkeet_hackathon_json/ |
The source exports, a public data set kept unchanged. 16 files, 15 questionnaires: the two esitietolomake-toisen-asteen-terveystarkastukseen-… files are identical exports. |
converter/ |
The converter (TypeScript on Node, run with tsx). |
converter/overrides/ |
The only hand-maintained input: per questionnaire, LOINC codes, structure, text corrections, retyped questions, and pre-population mappings, each with a $comment on why. |
fhir/ |
Generated; don't edit. questionnaires/ (one per survey, plus index.json, a Bundle of all), codesystems/ (the answer options), definitions/ (the project's two extensions), and the reports conversion-report.md, validation.md and terminology-check.md. |
app/ |
The web app (Vite, React 19, TypeScript). |
samples/bundles/ |
Synthetic patients: FinnishCore 2.0.0, personal identity codes from the test range. |
crosscheck/ |
Renders the Questionnaires in LHC-Forms, pre-populated from the samples through a local mock FHIR client. |
docs/ |
The implementation plan and progress. |
tools/ |
Git-ignored: the downloaded FHIR Validator and its output. |
| Command | What it does |
|---|---|
npm run dev |
Start the app at http://localhost:5173. |
npm run build |
Type-check and build the app into app/dist/. |
npm run convert |
Regenerate fhir/ from the exports and the overrides. A test fails if the committed fhir/ is out of date. |
npm run validate |
Run the HL7 FHIR Validator over fhir/ and samples/ (R4, SDC 3.0.0, FinnishCore 2.0.0) and write fhir/validation.md. Fails on any error. Needs Java and the validator (below). |
npm run verify-codes |
Look up every code in the overrides and samples at its source and write fhir/terminology-check.md: Finnish ICD-10, ATC and FinLOINC at THL's code server, LOINC at tx.fhir.org, SNOMED CT at the Nordic terminology server. Fails on any code not found or display that differs. Needs network access. |
npm run lint |
ESLint over the repository. |
npm run typecheck |
tsc in every workspace. |
npm test |
Vitest in every workspace: the converter, and the app with jsdom, Testing Library and axe-core. |
npm run check |
Lint, type-check and test. |
TypeScript is pinned to 6.0, because typescript-eslint doesn't support TypeScript 7 yet.
- Convert.
npm run convertloads each export strictly: an unknown or unsupported field stops it, rather than losing content. It then appliesconverter/overrides/{slug}.jsonand writes FHIR. Every decision and correction ends up infhir/conversion-report.md. - Render. The app fetches
fhir/questionnaires/index.json. It evaluatesenableWhen, and runs each score'scalculatedExpressionwith fhirpath.js, so what is tested is the expression in the Questionnaire. - Pre-populate. The patient data panel merges loaded Bundles into one store. "Fill in" then runs the SDC definitions in the Questionnaire:
observationLinkPeriod+codetake the latest final Observation within the period, converting unitsinitialExpressionwith launch context%patientandx-fhir-queryvariables, which are answered from the store as a FHIR server would- filled answers can enable follow-ups, which are then filled too; nothing the person answered is overwritten
- each answer records its source records, which are shown on screen and stored in the response
- Respond. The QuestionnaireResponse has the elements the Kanta PHR QuestionnaireResponse profile requires:
language, a narrative, anidentifier,subjectonce a patient is chosen, andauthoredonce completed.
npm run validate needs Java 17 or later and the HL7 FHIR Validator 6.10.4, which isn't committed (it's about 200 MB). Download it into tools/ and check it against the SHA-256 digest GitHub publishes for the release asset:
mkdir -p tools && curl -L -o tools/validator_cli.jar https://github.com/hapifhir/org.hl7.fhir.core/releases/download/6.10.4/validator_cli.jarshasum -a 256 tools/validator_cli.jarThe digest should be 1106b9d58f9e363e47bea7c4fc065841e5fc91fe9d062775c3bfdd212bd653cc. On its first run the validator downloads the FHIR packages it needs into ~/.fhir/packages.
cd crosscheck && npm installWith npm run dev running, open http://localhost:5173/@fs/<absolute path to the repository>/crosscheck/lhc-forms.html, choose a questionnaire and a sample, and press Render. The page lists the answers LHC-Forms produced and the searches it made. Nothing leaves the machine.
- Clinical review is needed. The pre-population mappings and the corrected translations are reasonable but not clinically reviewed; in particular, which diagnoses count for each illness option. They're all in
converter/overrides/, each with a comment. - Long-term illness relies on FinnishCore's
condition-permanencemark (or the problem list). Medications given as amedicationReferenceare matched through the Medication the search includes (_include); the LHC-Forms cross-check's mock client doesn't include them, so there they match by name only. SNOMED CT isn't used for matching, since Finnish diagnosis data is ICD-10. - The samples' dates are around 2026-10-13. As time passes, the observation-based answers fall outside their periods. The tests use a fixed date.
- Other renderers show UCUM unit codes (e.g. "wk") where this app shows the unit words (viikkoa). LHC-Forms doesn't fill date questions from
valueDateTimeObservations. - No persistence and no server: answers stay in the page until downloaded.