Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.12.1
v24.6.0
89 changes: 89 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
yarn start # Dev server at http://localhost:3000
yarn build # Production build
yarn test # Start test server (port 8080) + run all Cypress E2E tests headlessly
yarn test:search # Run only search specs
yarn test:breadcrumbs # Run only breadcrumb specs
yarn test:codeAttributes
yarn test:default
yarn test:popUp
yarn test-with-gui # Open Cypress interactive GUI
```

There is no lint script. Tests are Cypress E2E only — no Jest/unit tests exist.

## Architecture

### URL Structure

Two route shapes cover all catalogs:

```
/:language/:catalog/:version/:resource_type/:code # Versionized (ICD, CHOP, SwissDRG, TARMED, TARDOC, Reha, ZE)
/:language/:catalog/:resource_type/:code # Unversionized (MIGEL, AL, DRUG)
```

- `language`: `de` | `fr` | `it` | `en`
- `catalog`: `ICD` | `CHOP` | `SwissDRG` | `TARMED` | `TARDOC` | `AmbGroup` | `Reha` | `Supplements` | `MIGEL` | `AL` | `DRUG`

### State Management

No Redux/Zustand/Context. `App.tsx` is the sole state hub (class component). All state (`language`, `selectedButton`, `selectedVersion`, `searchResults`, `currentVersions`, etc.) lives there and flows down via props. Child components bubble changes back up via callback props.

### HOC Pattern for Class Components

Since the entire app uses class components but needs React Router v6 hooks (`useNavigate`, `useParams`) and `useTranslation`, every component wraps itself with an `addProps` HOC:

```typescript
function addProps(Component) {
return props => <Component {...props} navigation={useNavigate()} params={useParams()} translation={useTranslation()}/>;
}
export default addProps(MyClassComponent);
```

### API

Backend is hardcoded in `src/Utils.tsx`:
```typescript
export const fetchURL = 'https://search.eonum.ch'
```

All API calls use native `fetch()`. Key patterns:
- Versions: `GET /{lang}/{resource_type}/versions`
- Code detail (versionized): `GET /{lang}/{resource_type}/{version}/{code}?show_detail=1`
- Code detail (unversionized): `GET /{lang}/{resource_type}/{catalog}/{code}?show_detail=1&date={date}`
- Search: `GET /{lang}/{resource_type}/{version}/search?highlight=1&skip_sort_by_code=1&max_results={n}&search={term}`

The mapping between catalog names and `resource_type` strings lives in `src/Services/catalog-version.service.tsx`.

### Key Files

| File | Purpose |
|---|---|
| `src/App.tsx` | Root component, state hub, routing layout |
| `src/interfaces.ts` | All shared TypeScript interfaces |
| `src/Utils.tsx` | `fetchURL` constant, shared utilities |
| `src/i18n.tsx` | i18next setup; translations in `src/assets/translations/` |
| `src/Services/router.service.tsx` | URL parsing utilities |
| `src/Services/catalog-version.service.tsx` | Version fetching, catalog↔resource_type mapping |
| `src/Components/Bodies/` | `CodeBodyVersionized.tsx` and `CodeBodyUnversionized.tsx` — top-level page bodies |
| `src/Components/CodeAttributes/` | Attribute display components per catalog type |

### Coding Conventions (from README)

- Each class in its own file; all components under `src/Components/`, services under `src/Services/`
- Class names: first letter uppercase, rest lowercase (e.g. `Searchbar`)
- Method names: always lowercase
- Variable names: camelCase
- Constants: UPPERCASE
- Every method documented with JSDoc

### Responsive Layout

App implements manual responsive behavior: ≥1200px shows `ButtonGroup` + `Searchbar` side by side; below that, `Searchbar` appears first. `collapseMenu` state controls Bootstrap `<Collapse>` to hide/show search results on mobile.
135 changes: 74 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,79 @@
# README MEDCODESEARCH

### Brief description

This React-App is used to represent the different catalogs which gets updated every year or two. This website helps to
find the catalog number easier and to look up the different versions or expiry dates. \
Frontend from the website: [medcodesearch.ch](http://medcodesearch.ch) \
Backend which is used is: [search.eonum.ch](https://search.eonum.ch/documentation)

### Setup
#### Development
For the local installation go into the folder `medcodesearch-frontend-react` and run ` install`.
To start the local app run `yarn start`. It will open at [http://localhost:3000](http://localhost:3000) in your browser.
#### Production
Run deploy script `deploy.sh`.

### Coding conventions
Each class is defined in *its own file*. \
Everything has been written in English (Comments included). \
The first letter of a classname is in uppercase, the reminder is lowercase. \
Method-names are always lowercase. \
Variable-names are lowercase if only "oneword"-word, otherwise the first letter in between is capital. \
Constants are always uppercase. \
All components reside in their own subdirectory in `/src/Components`. \
All services reside in their own subdirectory in `/src/Services`. \
All test-suites reside in their own directory in `cypress/e2e`. \
Every method has its own documentation written in Javadoc.

### Testing
We use cypress for our tests. Since we use typescript, we also need babel for transformation.
#### Config
The configuration for babel and cypress are stored in babel.config.js and cypress.config.ts in the root folder and can
be adapted to your needs. We do frontend tests that can be run headless in terminal or, useful for debugging, run in
cypress GUI. To do so, we specified some custom commands in package.json under scripts section, namely

```json
{
"test": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress run'",
"test:search": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress run --spec cypress/e2e/searchMobile.cy.ts,cypress/e2e/search.cy.ts'",
"test:breadcrumbs": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress run --spec cypress/e2e/breadcrumbsMobile.cy.ts,cypress/e2e/breadcrumbs.cy.ts'",
"test:codeAttributes": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress run --spec cypress/e2e/codeAttributesMobile.cy.ts,cypress/e2e/codeAttributes.cy.ts,cypress/e2e/customCodeAttributes.cy.ts'",
"test:default": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress run --spec cypress/e2e/defaultMobile.cy.ts,cypress/e2e/default.cy.ts'",
"test:popUp": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress run --spec cypress/e2e/popUpMobile.cy.ts,cypress/e2e/popUp.cy.ts'",
"test-with-gui": "start-server-and-test start-test-server http-get://localhost:$npm_package_config_testPort 'cypress open'",
"start-test-server": "BROWSER=none PORT=$npm_package_config_testPort react-scripts start"
}
# medcodesearch-frontend-react

React frontend for [medcodesearch.ch](http://medcodesearch.ch) — a search interface for Swiss medical coding catalogs (ICD, CHOP, SwissDRG, TARMED, TARDOC, MIGEL, AL, DRUG, and more).

Backend API: [search.eonum.ch](https://search.eonum.ch/documentation)

---

## Setup

**Development**

```bash
yarn install
yarn start # http://localhost:3000
```

#### Run tests
Use `yarn test` to start headless server and tests or, for example `test:breadcrumbs` to run breadcrumbs tests only.
Currently the port for test server is set to `localhost:8080` in package.json. To start graphical tests use
`yarn test-with-gui`.
**Production**

```bash
./deploy.sh
```

---

## Architecture

All application state lives in `App.tsx` (a class component). Child components receive state via props and bubble changes back up via callbacks — no Redux or Context is used.

Because the app uses class components throughout but depends on React Router v6 hooks (`useNavigate`, `useParams`) and `useTranslation`, every component wraps itself in an `addProps` HOC that injects those hooks as props.

**Key files**

| Path | Purpose |
|------|---------|
| `src/App.tsx` | Root component and state hub |
| `src/interfaces.ts` | Shared TypeScript interfaces |
| `src/Utils.tsx` | `fetchURL` constant and shared utilities |
| `src/i18n.tsx` | i18next setup; translations in `src/assets/translations/` |
| `src/Services/router.service.tsx` | URL parsing utilities |
| `src/Services/catalog-version.service.tsx` | Version fetching and catalog↔resource_type mapping |
| `src/Components/Bodies/` | Top-level page bodies per catalog type |
| `src/Components/CodeAttributes/` | Attribute display components per catalog |

---

## Testing

Tests are Cypress E2E only (no unit tests). Cypress runs against a local dev server on port 8080.

```bash
yarn test # Run all tests headlessly
yarn test:search # Search specs only
yarn test:breadcrumbs # Breadcrumb specs only
yarn test:codeAttributes
yarn test:default
yarn test:popUp
yarn test-with-gui # Open Cypress interactive GUI
```

Cypress retries assertions automatically (default timeout: 4 s, configurable in `cypress.config.ts`).

---

## Coding conventions

- Each class in its own file; components under `src/Components/`, services under `src/Services/`, test suites under `cypress/e2e/`
- Class names: first letter uppercase, rest lowercase (e.g. `Searchbar`)
- Method names: lowercase
- Variable names: camelCase
- Constants: UPPERCASE
- Every method documented with JSDoc

With cypress, there is no need to use hard coded timeouts to wait for. If using `.should`, cypress automatically retries
the assertion for up to default timeout, before considering it a failure. The deault timeout is 4 seconds, but could be
changed in cypress.config.ts.
---

### Contact
For further question:
- +41 (0)31 311 17 06 -> eonum contact
- [info@eonum.ch](info@eonum.ch) -> eonum contact
- [jan.koch@students.unibe.ch](jan.koch@students.unibe.ch) -> university development team
- [eonum.ch/de/kontakt/](https://eonum.ch/de/kontakt/) -> eonum website
## Contact

### Diagram
![img.png](img.png)
- [info@eonum.ch](mailto:info@eonum.ch)
- [eonum.ch/de/kontakt/](https://eonum.ch/de/kontakt/)
7 changes: 1 addition & 6 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
module.exports = {};
37 changes: 23 additions & 14 deletions cypress/e2e/codeAttributesMobile.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
describe('Code attributes test suite for desktop version', function () {
describe('Code attributes test suite for mobile version', function () {
const baseUrl = Cypress.config("baseUrl");
const backendUrl = 'https://search.eonum.ch';
let drgB02A: any;

before(() => {
cy.request(`${backendUrl}/de/drgs/V12.0/B02A?show_detail=1`).then((res) => {
drgB02A = res.body;
});
});

beforeEach(() => {
cy.viewport(400, 800);
Expand Down Expand Up @@ -34,31 +42,32 @@ describe('Code attributes test suite for desktop version', function () {
});

it('show drg code information', function () {
cy.visit(baseUrl + '/de/SwissDRG/V12.0/drgs/B02A?query=A06A');
cy.contains("Durchschnittliche Verweildauer (Tage)").should('be.visible');
cy.contains("15.4").should('be.visible');
cy.contains("Erster Tag mit Abschlag").should('be.visible');
cy.contains("Abschlag pro Tag").should('be.visible');
cy.contains("0.845").should('be.visible');
cy.contains("Erster Tag mit Zuschlag").should('be.visible');
cy.contains("29").should('be.visible');
cy.contains("Zuschlag pro Tag").should('be.visible');
cy.contains("0.289").should('be.visible');
cy.visit(baseUrl + '/de/SwissDRG/V12.0/drgs/B02A');
cy.get("#attributesTable").within(() => {
cy.contains("Durchschnittliche Verweildauer (Tage)").should('be.visible');
cy.contains(drgB02A.average_stay_duration.toFixed(1)).should('be.visible');
cy.contains("Erster Tag mit Abschlag").should('be.visible');
cy.contains("Abschlag pro Tag").should('be.visible');
cy.contains(drgB02A.discount_per_day.toFixed(3)).should('be.visible');
cy.contains("Erster Tag mit Zuschlag").should('be.visible');
cy.contains(String(drgB02A.first_day_surcharge)).should('be.visible');
cy.contains("Zuschlag pro Tag").should('be.visible');
cy.contains(drgB02A.surcharge_per_day.toFixed(3)).should('be.visible');
});

cy.visit(baseUrl + '/fr/SwissDRG/V12.0/drgs/B02A?query=A06A');
cy.visit(baseUrl + '/fr/SwissDRG/V12.0/drgs/B02A');
cy.contains("Durée de séjour moyenne (journées)").should('be.visible');
cy.contains("Premier jour avec réduction").should('be.visible');
cy.contains("Réduction journalier").should('be.visible');
cy.contains("Premier jour avec supplément").should('be.visible');
cy.contains("Supplément journalier").should('be.visible');

cy.visit(baseUrl + '/it/SwissDRG/V12.0/drgs/B02A?query=A06A');
cy.visit(baseUrl + '/it/SwissDRG/V12.0/drgs/B02A');
cy.contains("Durata media di degenza (giorni)").should('be.visible');
cy.contains("Primo giorno con riduzione").should('be.visible');
cy.contains("Tasso di riduzione giornaliero").should('be.visible');
cy.contains("Primo giorno con supplemento").should('be.visible');
cy.contains("Supplemento giornaliero").should('be.visible');
cy.contains("Elementi simili").should('be.visible');
});

it('SL code information', function () {
Expand Down
42 changes: 22 additions & 20 deletions cypress/e2e/customCodeAttributePages.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
describe('Code attributes test suite for desktop version and components with custom page', function () {
const baseUrl = Cypress.config("baseUrl");
const backendUrl = 'https://search.eonum.ch';
let drgI06A: any;

before(() => {
cy.request(`${backendUrl}/de/drgs/V13.0/I06A?show_detail=1`).then((res) => {
drgI06A = res.body;
});
});

beforeEach(() => {
cy.viewport(1366, 768);
Expand All @@ -12,10 +20,8 @@ describe('Code attributes test suite for desktop version and components with cus

cy.visit(baseUrl + "/de/SwissDRG/V13.0/drgs/I06A");
cy.get('#drgOnlineManualLink')
// Check href attribute value
.should('have.attr', 'href', 'https://manual.swissdrg.org/de/13.3/drgs/I06A')
cy.get('#drgDynamicsLink')
// Check href attribute value
.should('have.attr', 'href', 'https://drgdynamics.eonum.ch/drgs/name?code=I06A&version=V13.0&locale=de')

cy.get("#attributesTable tr th").then(($ths) => {
Expand All @@ -27,24 +33,20 @@ describe('Code attributes test suite for desktop version and components with cus
cy.get("#attributesTable tr td").then(($tds) => {
// @ts-ignore
const tds = [...$tds].map((td) => td.innerText);
expect(tds[0]).to.equal("Partition");
expect(tds[1]).to.equal("O");
expect(tds[2]).to.equal("Kostengewicht");
expect(tds[3]).to.equal("8.137");
expect(tds[4]).to.equal("Durchschnittliche Verweildauer (Tage)");
expect(tds[5]).to.equal("18.9");
expect(tds[6]).to.equal("Erster Tag mit Abschlag");
expect(tds[7]).to.equal("5");
expect(tds[8]).to.equal("Abschlag pro Tag");
expect(tds[9]).to.equal("0.697");
expect(tds[10]).to.equal("Erster Tag mit Zuschlag");
expect(tds[11]).to.equal("33");
expect(tds[12]).to.equal("Zuschlag pro Tag");
expect(tds[13]).to.equal("0.242");
expect(tds[14]).to.equal("Verlegungsfallpauschale");
expect(tds[15]).to.equal("Nein (0.244)");
expect(tds[16]).to.equal("Ausnahme von Wiederaufnahme");
expect(tds[17]).to.equal("Nein");
// Build a label→value map (rows alternate: label, value, label, value, ...)
const tableMap: Record<string, string> = {};
for (let i = 0; i < tds.length - 1; i += 2) {
tableMap[tds[i]] = tds[i + 1];
}
expect(tableMap["Partition"]).to.equal(drgI06A.partition_letter);
expect(tableMap["Kostengewicht"]).to.equal(drgI06A.cost_weight.toFixed(3));
expect(tableMap["Durchschnittliche Verweildauer (Tage)"]).to.equal(drgI06A.average_stay_duration.toFixed(1));
expect(tableMap["Erster Tag mit Abschlag"]).to.equal(String(drgI06A.first_day_discount));
expect(tableMap["Abschlag pro Tag"]).to.equal(drgI06A.discount_per_day.toFixed(3));
expect(tableMap["Erster Tag mit Zuschlag"]).to.equal(String(drgI06A.first_day_surcharge));
expect(tableMap["Zuschlag pro Tag"]).to.equal(drgI06A.surcharge_per_day.toFixed(3));
expect(tableMap["Verlegungsabschlag pro Tag"]).to.equal(drgI06A.transfer_discount.toFixed(3));
expect(tableMap["Ausnahme von Wiederaufnahme"]).to.equal(drgI06A.exception_from_reuptake ? "Ja" : "Nein");
});

cy.contains("Komplexe Eingriffe an der Wirbelsäule mit äusserst schweren CC und Alter < 16 Jahre oder sehr komplexe WS-Eingriffe oder intensivmedizinischer Komplexbehandlung/IMCK > 184 Aufwandspunkte oder geriatrische Akutrehabilitation ab 14 Behandlungstage").should('exist');
Expand Down
Loading