Skip to content

tpximpact/pdmedcalc-v2

Repository files navigation

README

Introduction

This repository is for the MedCalc tool, a web application that helps healthcare providers quickly and easily convert a Parkinson’s patient’s regular medicine into an equivalent dose of another medicine.

The need for this tool is based on the fact that sometimes patients with Parkinson's Disease (PD) who have been admitted to hospital are unable to take their medications orally. Therefore the patient's usual oral medications need to be converted to a 'Levodopa equivalent dose' (LED), which is delivered either through a patch or via a naso-gastric tube.

Application programming overview

The application is written in JavaScript and uses the Next.js framework and React.js library.

The Next.js framework is configured for static export. This means the compiled application runs entirely in the browser and has no server side execution.

Stylesheets are written using the Sass

There is no database component. The details of the drugs involved in the calculation are defined in the file src/app/data/data.js.

Version control

The application uses the Git version control system and the GitFLow branching strategy.

The code is hosted on GitHub. The location of the repository is: https://github.com/tpximpact/pdmedcalc-v2

The production branch is called main The development branch is called develop

Hosting and deployment

The application is hosted on CloudFlare pages. CloudFlare is also used as the NameServer and provides security protection for the site including DDOS protection.

CloudFlare Pages also provides the deployment pipeline. The CloudFlare pages application is connected to the GitHub repository. A deployment is triggered when code is committed to the main or develop branch. The application is automatically tested and built as part of the deployment pipeline.

Code changes to the main branch are deployed to production. Code changes to any other branch are deployed to a test environment.

The production URL is: https://pdmedcalc.co.uk

A new test environment is created each time code is pushed to a non production branch. The test URL follows the pattern: https://[commit-hash].pdmedcalc-v2.pages.dev/ e.g. https://7047263d.pdmedcalc-v2.pages.dev/

Note: There isn't a configuration file in the repository for CloudFlare. Configuration is provided inside the CloudFlare pages interface.

Local set up

  1. Clone the repo to your machine
  2. cd into the directory where you cloned it
  3. npm install to install the relevant JavaScript dependencies.

How to start the dev server locally

Run npm run dev to start the development server and visit http://localhost:3000 to see the home page

How to run the tests

Run npm run test to run the jest (unit and integration) tests

Run npm run cypress:open to run the cypress (end-to-end) tests

How to build the static application

Run npm run build

The static assets can then be found in the generated out folder.

Deployment script

The automated deployment script which runs when a deployment is triggered is:

npm run test && npm run build

Note: this is configured inside the CloudFlare pages interface, there isn't a separate build script file or hosting configuration file.

CloudFlare Analytics

CloudFlare Analytics is integrated with the site for the purposes of counting the number of users. It can be accessed on the Web Analytics part of the CloudFlare Dashboard.

Cookies

The site does not set cookies or store any activity undertaken by the users.

Paid for components

There are no commercially licensed software libraries in this application. The CloudFlare hosting environment is being used on the free tier. CloudFlare Analytics is being used on the free tier. The production domain registration is being provided by 123-reg.co.uk

Version

This project is based upon an older version of the MedCalc.

This is therefore version 2 of the MedCalc tool.

How it works

Program flow

program-flow
  • Users land on the home page, read the information presented and accept the disclaimer. This redirects the user to the calculator page.
  • On the calculator page, users enter the respective medications and frequencies using the select dropdowns and the add button. Once the user is done adding medicines they then submit the form by clicking on the calculate button. This redirects the user to the results page.
  • The medicines and frequencies the user entered on the calculator page are on the search params of the url. If these are missing (for example because the user went straight to /results and skipped the calculator page), the user is automatically redirected to the home page. If however they are present as expected, then the results page uses them to present to the user:
    1. what was entered on the calculator page
    2. a calculation of the total levodopa equivalent dose (in mg per day)
    3. a conversion of the entered medications to a dispersible madopar split across four dose times i.e. option 1
    4. a conversion of medications to a transdermal rotigotine patch i.e. option 2

The calculation rules

See the 'rules of the calculator doc' for a detailed explanation written by James Fisher.

Implementation

Although this project uses javascript rather than typescript, the types for the expected inputs and outputs of the various functions are nonetheless described for the sake of clarity.

Data storage

No user data is stored. The only data this app stores is related to the medicines themselves.

This app therefore does not have a database because it does not really need one since there are only 56 different medicines and we only need to know three different properties of each; having a database for the sake of one table with four columns and just 56 entries is unnecessary.

The data related to the medicines is in src/app/data/data.js.

The medications object

The medications object is of the type :

type Medications = {
  string : {
    led: number
    isDa: boolean
    isComt: boolean
    hasLevodopa: boolean
  }
} 

Each property of the medications object is a string representing the name of that medicine. The value of this property is an object with led, isDa and isComt properties.

led represents the levodopa equivalent dose for one unit of this medicine.

isDa represents whether or not this medicine is a dopamine agonist.

isComt represents whether or not this medicine is a comt inhibitor.

hasLevodopa determines if a mediciene contains Levodopa. Levodopa mediciens are affected by the presense of a comt inhibitor.

Functions

All the functions responsible for the actual conversion of the medicines are in src/app/calculator/calculator-functions.js.

These functions are written in a functional programming style; they are pure i.e. they take an input and return an output without mutating the original input or having any other side-effect.

The mainTransform function
mainTransform

The mainTransform function takes an array of medicine objects. Each medicine object is formed by looping through the search params of the results page's url and is of the type

{
  name: string
  frequencyPerDay: number
}

The two options object that the mainTransform function returns is of the type

{
  option1: TimeMadoparObject
  option2: number
}

where

type TimeMadoparObject = {
  "0800": TimeMadparTuple
  "1200": TimeMadparTuple
  "1600": TimeMadparTuple
  "2000": TimeMadparTuple
}

and

type TimeMadparTuple = [
  { name: 'Madopar Dispersible 125mg (25/100mg)', quantity: number, led: 100 },
  { name: 'Madopar Dispersible 62.5mg (12.5/50mg)', quantity: number, led: 50 }
]
The calculateRotigotine function
calculateRotigotine

The calculateRotigotine function takes an array of medicine objects and returns a number representing the equivalent patchdose in mg for these medicines.

The patchdose is calculated by:

  • splitting out the dopamine agonists from the non-dopamine agonists
  • calculating the total led of the dopamine agonists and the total led of the non-dopamine agonists
  • dividing the totalLedOfDopamineAgonists by the adjustment (30)
  • multiplying the totalLedOfNonDopamineAgonists by the correction factor (0.25) and then divding the result by the adjustment (30)
  • summing the results of the previous two steps
  • rounding the result to a multiple of 2
The calculateTotalLed function
calculateTotalLed

The calculateTotalLed function takes an array of medicine objects and returns a number representing the total levodopa equivalent dose for these medicines.

This total led is calculated by:

  • splitting out the comt inhibtors from the non-comt inhibitors
  • calculating the total led of the non-comt inhibitors by multiplying the frequencyPerDay of each medicine with its led and all the results
  • picking out the comt inhibitor with the largest totalLedAdjustment* and multiplying its totalLedAdjustment against the totalLedFromNonComtInhibitors
  • summing the totalLedFromNonComtInhibitors and the result of the previous step

*Note that, in practice, "patients would only ever be on one COMT inhibitor, since [...] wouldn't prescribe more than one of them as there would be no value in doing so (once the COMT enzyme is blocked, it's blocked)". The function nevertheless handles the case where a user might input multiple comt inhibitors on the calculator page

The calculateMadopar function
calculateMadopar

The calculateMadopar function takes a targetLED and returns a TimeMadoparObject representing an equivalent total led in the form of dispersible madopar split across four dose times.

This is calculated by:

  • rounding the targetLED to the nearest 50 since the dispersible madopar comes in two sizes 50 (small) and 100 (big)
  • finding every possible combination of big and small madopars whose totalLed equals the roundedTargetLed
  • for each of the above combinations, allocating the big and small madopars to four time zones to create a TimeMadoparObject for each
  • for each TimeMadoparObject above calculating the maximum spread between the total leds of the four dose times
  • returning the TimeMadoparObject which has the lowest max spread (i.e. the one in which the doses are most evenly spread across the four time slots)
The allocateMadopar function
allocateMadopar

The allocateMadopar function takes two arguments; noOfBigMadopars and noOfSmallMadopars. It returns a TimeMadoparObject representing an equivalent total led in the form of dispersible madopar split across four dose times.

This allocation is done by:

  • looping through the big madopars and placing one in each time slot (starting at 0800) until there are none left
  • looping through the small madopars and placing one in each time slot (starting at the time slot which has the fewest big madopars) until there are none left
The calculateMaxSpread function
calculateMaxSpread

The calculateMaxSpread function takes a TimeMadoparObject and returns a number representing the maximum difference in total leds between the time slot with the highest total led and the time slot with the lowest total led.

This is calculated by:

  • calculating the total led for each time slot
  • calculating the difference in total leds between each time slot
  • returning the maximum difference in total led between the time slots

Tests

Testing strategy

Given that this is a medical tool, the need for it to be accurate and reliable is obvious.

The following testing strategy has therefore been implemented:

  1. manual/user acceptance testing by a clinician prior to go live
  2. integration tests for the mainTransform function
  3. end-to-end tests covering the full user journey from the home page to the results page
  4. unit tests for all the other functions that build up to the mainTransform function

Manual/user acceptance testing

To give confidence that the tool is correctly calculating the conversions for the various medicines, a trained medical professional has created example patients and calculated by hand what the correct dose output should be for each. These have then been compared against the results that the tool produces to make sure the two align.

Integration testing

Since the mainTransform function is ultimately responsible for taking the array of medicines entered on the calculator page as an argument and returning the output (conversion to dispersible madopar and transdermal rotigotine patch) which will be shown on the results page, this is the most important function to test.

Proving its robustness also has the nice side effect of simultaneously proving that all the other functions which mainTransform relies on are themselves working correctly.

Test cases

There are 61 different medicines a user could potentially enter on the calculator page. For each medicine entered there are 10 possible frequencies they could select. If we think of a medicine/frequency pair as one "thing", then there are effectively 610 different "things" that could be entered.

Combinatorics allows us to calculate the number of different possible combinaitions of these 610 "things".

The combinations formula is:

C(n,r) = n!/r!(n-r)!

where n is the total number of things you are choosing from, r is the number of things you are choosing and ! is factorial.

Suppose that a user enters 3 different "things". 610!/3!(610 - 3)! is the same as 610 * 609 * 608 / 3! which equals 37,644,320.

This illustrates the vast number of possibilities. And that's just with the user entering 3 different medicine/frequency pairs.

It's obviously not feasible to write over 37 million integration tests so we need to reduce the number of "things" while retaining confidence in our tests and app.

The frequency only affects the overall conversion via the calculateTotalLed function and so, given that this function is itself unit tested, we don't have to consider the frequencies in the integration test cases thus reducing the number of "things" back down to a more manageable 61.

Nevertheless, 61!/3!(61 - 3)! is still quite big (35,990) and writing thirty-five thousand integration tests would also not be practical.

We need to reduce the number of "things" further (while still maintaining confidence in our tests and app).

The overall conversion treats medcines differently based on whether they are a dopamine agonist, a comt-inhibitor or neither. Therefore we in effect have just 3 "things" to create test cases for.

  • If a user enters just one medicine, there are three possibililities (3!/1!(3 - 1)! = 3) i.e. they entered a dopamine agonist or they entered a comt inhibitor, or they entered a medicine that is neither a dopamine agonist nor a comt inhibitor
  • If a user enters two medicines, there are three possibililities (3!/2!(3 - 2)! = 3) i.e. they entered a dopamine agonist and a comt inhibitor, or they entered a dopamine agonist and a medicine that is neither a dopamine agonist nor a comt inhibitor, or they entered a comt inhibitor and a medicine that is neither a dopamine agonist nor a comt inhibitor
  • If a user enters three medicines, there is just one possibility (3!/3!(3 - 3)! = 1) i.e. they entered a dopamine agonist, a comt inhibitor and a medicine that is neither a dopamine agonist nor a comt inhibitor

Thus we can see that we only have to write 7 integration tests rather than tens of millions!

These 7 tests are in src/app/tests/integration-tests/mainTransform.test.js.

End to end testing

Cypress is used to run end-to-end tests on the actual live site. It uses the same 7 test cases as the integration tests.

These end-to-end tests are in cypress/e2e/home-calc-results-flow.cy.js

Unit testing

Each of the individual functions that feed into the mainTransform function are themselves unit tested. These unit tests can be found in src/app/tests/unit-tests.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •